从一道面试题引发的Java线程通信的思考

一、前言

线程通信是Java并发编程中的基础。曾经面试的时候遇到一个比较有趣的面试题拿出来与大家分享。

题目:

2个线程A和B,A线程生产A数据,然后消费B数据;B线程生产B数据,然后消费A数据,请用代码实现。

题目分析:

这是个典型的线程间通信的题目,由于AB分别需要消费对方生产的数据,需要AB在生产完成后通知对方线程消费,同时也需要在没有数据的时候阻塞等待。有没有觉得很熟悉?类似交替打印的问题?

二、题解

下边,我将用2种思路进行解答。

思路一: wait/notify

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* ThreadA--生产A--打印B--start
* ThreadB--生产B--打印A--start
*
* 执行结果
* thread-B:A
* thread-A:B
*/
public class ThreadLockDemo2 {

private String A = null;
private String B = null;

private Object lock = new Object();

public void doA() {
// 生产A
A = "A";

// 假设A获得锁,则B阻塞等待
synchronized (lock) {
if (B == null) {
try {
// 只有拥有对象锁,才能调用对象的wait()方法让当前线程阻塞(进入monitor的等待队列),该方法会释放对象锁。
// 该线程必须有其他线程通过notify()或者notifyAll()唤醒
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// A线程被B线程notify唤醒,顺利消费B
System.out.println(Thread.currentThread().getName() + ":" + B);
lock.notify();
}
}

public void doB() {
// 生产B
B = "B";
// A释放锁后,B可以获得monitor的所有权
synchronized (lock) {
if (A == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// B顺利消费A生产的数据,然后唤醒A线程
System.out.println(Thread.currentThread().getName() + ":" + A);
lock.notify();
}
}


public static void main(String[] args) {

ThreadLockDemo2 threadLockDemo = new ThreadLockDemo2();
new Thread(new Runnable() {
@Override
public void run() {
threadLockDemo.doA();
}
}, "thread-A").start();

new Thread(new Runnable() {
@Override
public void run() {
threadLockDemo.doB();
}
}, "thread-B").start();
}
}

思路二: Condition/await/signal

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* ThreadA--生产A--打印B--start
* ThreadB--生产B--打印A--start
*
* 执行结果
* thread-B:A
* thread-A:B
*/
public class ThreadLockDemo {

private String A = null;
private String B = null;

private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();

public void doA() {
// 假设A线程先获得锁。则B会阻塞在此等待锁释放
lock.lock();
try {
// 生产A
A = "A";
if (B == null) {
// 会阻塞A线程,并让该Condition关联的锁释放(阻塞在lock方法处的线程B重新获取锁)
conditionA.await();
}
// 打印B
System.out.println(Thread.currentThread().getName() + ":" + B);

// 唤醒B线程
conditionB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void doB() {
// 现成B由于conditionA.await而获得锁
lock.lock();
try {
// 生产B
B = "B";
if (A == null) {
conditionB.await();
}
// 打印A
System.out.println(Thread.currentThread().getName() + ":" + A);

// 唤醒该Condition关联的线程A
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}


public static void main(String[] args) {
ThreadLockDemo threadLockDemo = new ThreadLockDemo();
new Thread(new Runnable() {
@Override
public void run() {
threadLockDemo.doA();
}
}, "thread-A").start();

new Thread(new Runnable() {
@Override
public void run() {
threadLockDemo.doB();
}
}, "thread-B").start();
}
}

三、注意

  1. 当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞。(这种阻塞是通过提前释放synchronized锁,进入等待队列导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)

  2. 调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程; (notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)

  3. 调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度

下边我们来验证下2.

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 执行结果:
*
* Thread-A wait before...
* Thread-B i=0
* Thread-B i=1
* Thread-B i=2
* Thread-B notify...
* Thread-B i=3
* Thread-B i=4
* Thread-B i=5
* Thread-A wait after...
*
* 由结果可以看出,线程B执行notify之后线程A并没有立即获得锁,
* 而是等待B释放锁后。
*/

public class WaitDemo {

private int index = 0;
Object lock = new Object();

public void method1() {
try {
synchronized (lock) {
if (index != 3) {
System.out.println(Thread.currentThread().getName() + " wait before...");
lock.wait();
System.out.println(Thread.currentThread().getName() + " wait after...");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void method2() {
try {
synchronized (lock) {
for (int i = 0; i < 6; i++) {
index ++;
if (i == 3) {
lock.notify();
System.out.println(Thread.currentThread().getName() + " notify...");
}
System.out.println(Thread.currentThread().getName() + " i=" + i);
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
WaitDemo waitDemo = new WaitDemo();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
waitDemo.method1();
}
}, "Thread-A");
threadA.start();

// 注意,这里为了模拟让A线程先执行,此处sleep 1s。(如果B先执行则A wait后无法唤醒)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
waitDemo.method2();
}
}, "Thread-B");
threadB.start();

}
}