一道面试中常考的并发编程的代码题:

  • 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…….
  • 两个线程交替打印1-100的奇偶数
  • N个线程循环打印1-100
  • ……

其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。

synchronized+wait/notify

基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num的初始值为0,所以线程B或线程C拿到锁后,进入while()循环,然后执行wait()方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()循环,执行num++,打印字符A,最后唤醒线程B和线程C。此时num值为1,只有线程B拿到锁后,不被阻塞,执行num++,打印字符B,最后唤醒线程A和线程C,后面以此类推。

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
public class Wait_Notify_ABC {
private volatile int num;
private static final Object Lock = new Object();

private void printABC(int tagertNum){
for(int i = 0; i < 10; ++i){
synchronized (Lock){
while (num % 3 != tagertNum){
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName());
Lock.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_ABC wait_Notify_ABC = new Wait_Notify_ABC();
Thread threadA = new Thread(() -> wait_Notify_ABC.printABC(0), "A");
Thread threadB = new Thread(() -> wait_Notify_ABC.printABC(1), "B");
Thread threadC = new Thread(() -> wait_Notify_ABC.printABC(2), "C");
threadA.start();
threadB.start();
threadC.start();
}
}

输出结果:

1
2
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。

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
class  Wait_Notify_Odd_Even{
private Object monitor = new Object();
private volatile int count;
Wait_Notify_Odd_Even(int initCount) {
this.count = initCount;
}
private void printOddEven() {
synchronized (monitor) {
while (count < 10) {
try {
System.out.print( Thread.currentThread().getName() + ":");
System.out.println(++count);
monitor.notifyAll();
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
monitor.notifyAll();
}
}

public static void main(String[] args) throws InterruptedException {
Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
new Thread(waitNotifyOddEven::printOddEven, "odd").start();
Thread.sleep(10); //为了保证线程odd先拿到锁
new Thread(waitNotifyOddEven::printOddEven, "even").start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10

再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:

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
class Wait_Notify_100 {
private int num;
private static final Object LOCK = new Object();
private int maxnum = 10;
private void printABC(int targetNum) {
while (true) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
if(num >= maxnum){
break;
}
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(num >= maxnum){
break;
}
num++;
System.out.println(Thread.currentThread().getName() + ": " + num);
LOCK.notifyAll();
}
}
}

public static void main(String[] args) {
Wait_Notify_100 wait_notify_100 = new Wait_Notify_100 ();
new Thread(() -> {
wait_notify_100.printABC(0);
}, "thread1").start();
new Thread(() -> {
wait_notify_100.printABC(1);
}, "thread2").start();
new Thread(() -> {
wait_notify_100.printABC(2);
}, "thread3").start();
}
}

输出打印:

1
2
3
4
5
6
7
8
9
10
thread1: 1
thread2: 2
thread3: 3
thread1: 4
thread2: 5
thread3: 6
thread1: 7
thread2: 8
thread3: 9
thread1: 10

Lock

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
 class Lock_ABC {

private int num; // 当前状态值:保证三个线程之间交替打印
private Lock lock = new ReentrantLock();


private void printABC(int targetNum) {
for (int i = 0; i < 10; ) {
lock.lock();
if (num % 3 == targetNum) {
num++;
i++;
System.out.print(Thread.currentThread().getName());
}
lock.unlock();
}
}

public static void main(String[] args) {
Lock_ABC lockABC = new Lock_ABC();

new Thread(() -> {
lockABC.printABC(0);
}, "A").start();

new Thread(() -> {
lockABC.printABC(1);
}, "B").start();
new Thread(() -> {
lockABC.printABC(2);
}, "C").start();
}
}

输出日志:

1
ABCABCABCABCABCABCABCABCABCABC

Lock+Condition

使用Lock+Condition实现对线程的精准唤醒,减少对同步锁的无意义竞争,浪费资源。

该思路和synchronized+wait/notify方法的很像,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。

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
class LockConditionABC {

private int num;
private static Lock lock = new ReentrantLock();
private static Condition c1 = lock.newCondition();
private static Condition c2 = lock.newCondition();
private static Condition c3 = lock.newCondition();

private void printABC(int targetNum, Condition currentThread, Condition nextThread) {
for (int i = 0; i < 10; ) {
lock.lock();
try {
while (num % 3 != targetNum) {
currentThread.await(); //阻塞当前线程
}
num++;
i++;
System.out.print(Thread.currentThread().getName());
nextThread.signal(); //唤醒下一个线程,而不是唤醒所有线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

public static void main(String[] args) {
LockConditionABC print = new LockConditionABC();
new Thread(() -> {
print.printABC(0, c1, c2);
}, "A").start();
new Thread(() -> {
print.printABC(1, c2, c3);
}, "B").start();
new Thread(() -> {
print.printABC(2, c3, c1);
}, "C").start();
}
}

输出结果:

1
ABCABCABCABCABCABCABCABCABCABC