Wilder's Blog.

并发编程总结五

字数统计: 1.9k阅读时长: 7 min
2018/09/12 Share

并发编程总结五

1、现在有线程T1、T2和T3。你如何确保T2线程在T1之后执行,并且T3线程在T2之后执行?
这个问题主要是考察 join( ) 方法的应用,如果一个线程A执行了 thread.join() 语句,其含义是:当前线程A等待 thread 线程终止之后才从 thread.join() 返回。也就是说,当线程A执行了线程thread的 join 方法时,会先执行线程thread中的内容,等待从join方法返回之后,才会继续执行线程A方法剩下的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 public class JoinLearn{
static class MyThread implements Runnable{
private Thread thread;
public MyThread(Thread thread){
this.thread = thread;
}

@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在运行......");
}
}

public static void main(String[] args){
Thread previous = Thread.currentThread();
Thread t1 = new Thread(new MyThread(previous), String.valueOf(1));
t1.start();
Thread t2 = new Thread(new MyThread(t1), String.valueOf(2));
t2.start();
Thread t3 = new Thread(new MyThread(t2), String.valueOf(3));
t3.start();
}
}
2、Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势
  • Lock接口提供了与synchronized类似的同步功能,只是在使用时需要显示地获取锁和释放锁
  • 拥有了获取锁与释放锁的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。Lock多了锁投票,定时锁等候和中断锁等候等特性。

有这么一种情况,线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定。如果使用synchronized ,如果A不释放,B将会一直等待下去,而且B的等待不可以被中断。如果使用了 ReentrantLock ,如果A不释放,可以使B在等待了足够长的时间以后中断等待,而干别的事情。

接下来我们来说一下两者的用法:
  • synchronized 可以锁定一个方法,也可以锁定一个对象,synchronized不需要自己定义获取锁和释放锁,这些都是隐式的。一旦线程A获取到锁之后,就只有线程A可以访问synchronized修饰的这个部分,只有等到线程A释放锁之后,其他线程才有访问的机会。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Sync implements Runnable{
private Data data = new Data();

@Override
public synchronized void run() {
int tmp = data.getmVal();
++tmp;
data.setmVal(tmp);
System.out.println(Thread.currentThread().getName()+"|"+data.getmVal());
}

public static void main(String[] args) {
Sync myThread = new Sync();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(myThread);
thread.start();
}
}
}

运行结果为:

1
2
3
4
5
Thread-0|1
Thread-1|2
Thread-2|3
Thread-3|4
Thread-4|5

如果不使用synchronized修饰时,可能会产生如下结果:

1
2
3
4
5
Thread-0|2
Thread-1|2
Thread-2|3
Thread-4|4
Thread-3|5
  • Lock 由于需要自己手动获取锁和释放锁,当代码出现异常时,有可能因为下面的代码执行不到导致锁无法释放,因此要把释放锁放在finally中,我们同样适用上面synchronized的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LockLearn implements Runnable{
Lock lock = new ReentrantLock();
Data data = new Data();

@Override
public void run() {
lock.lock();
try {
int tmp = data.getmVal();
++tmp;
data.setmVal(tmp);
System.out.println(Thread.currentThread().getName()+"|"+tmp);
}finally {
lock.unlock();
}
}

public static void main(String[] args) {
LockLearn lock = new LockLearn();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(lock);
thread.start();
}
}
}
3、Java 中 wait 和 sleep 方法有什么区别
根据我的理解以及查阅其它博客,大致总结一下 wait 和 sleep 的区别:
sleep
  • 让当前线程进入停滞状态(即阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。
  • 在一个synchronized块中调用 sleep 方法时,线程虽然休眠但是不会释放锁。
  • 在sleep 休眠时间满之后,该线程并不一定会立即执行,这是因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait
  • wait 方法不是线程中的方法,这个方法属于 Object 类;当一个线程执行 wait 方法时,它就进入到一个和该对象相关的等待池中,同时会释放锁。
  • wait 使用 notify 或者 notifyAll 或者指定的睡眠时间来唤醒当前等待池中的线程
  • wait 必须放在 synchronized block 中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
4、如何在 Java 中实现一个阻塞队列
阻塞队列和普通队列的区别就是,当队列中没有元素的时候,调用出队方法的线程将会进入等待状态,直到队列中存在元素时线程会再次被唤醒;当队列已满的时候,调用入队方法的线程将会进入等待状态,直到有线程从队列中出队时该线程才会被唤醒。

我们使用 wait 和 notifyAll 方法来实现一个基本的阻塞队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BlockingQueue {
private List queue = new LinkedList();

/**
* 初始化队列大小
*/
private int limit = 10;
public BlockingQueue(int limit){
this.limit = limit;
}

public synchronized void enqueue(Object item) throws InterruptedException {
while (this.queue.size() == limit){
//说明队列已满
wait();
}
if (this.queue.size() == 0){
notifyAll();
}
this.queue.add(item);
}

public synchronized Object dequeue() throws InterruptedException {
//队列为空不能出队
while (this.queue.size() == 0){
wait();
}
if (this.queue.size() == this.limit){
notifyAll();
}

return this.queue.remove(0);
}
}
5、写一段死锁代码。

说来就来,上死锁代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";

public static void main(String[] args) {
new DeadLockDemo().deadLock();
}

private void deadLock(){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
System.out.println("1");
}
}
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
System.out.println("2");
}
}
}
});

t1.start();
t2.start();
}
}

从代码上可以看出,线程t1拿到了代码块A的锁,t2拿到了代码块B的锁,后来t1想拿到B的锁,t2想拿到A的锁,但是两条线程都不会释放锁,所以两者都陷入了等待状态,变成了死锁。

6、 既然 start() 方法会调用 run() 方法,为什么我们调用 start() 方法,而不直接调用 run() 方法?
start( ):他的作用是启动一个新线程,新线程会执行响应的 run( ) 方法,start( ) 不能被重复调用

run( ): run( ) 就和普通的成员方法一样,可以被重复调用。单独调用run( )的话,会在当前线程中执行run( ),而不会启动新的线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RunAndStartDemo implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" is running");
}

public static void main(String[] args) {
RunAndStartDemo demo = new RunAndStartDemo();

Thread thread = new Thread(demo);
System.out.println("调用run()方法...");
thread.run();
System.out.println("调用start()方法...");
thread.start();
}
}

运行结果为:

1
2
3
4
调用 run() 方法...
main is running
调用 start() 方法...
Thread-0 is running
8. Java 中 volatile 关键字是什么?你如何使用它?它和 Java 中的同步方法有什么区别?

自从 Java 5 中调整 volatile 关键字和 Java 内存模型后,有关 volatile 关键字的线程问题越来越常见。掌握 volatile变量在并发环境中如何确保可见性、有序性和一致性非常重要。

CATALOG
  1. 1. 并发编程总结五