有一个非常著名的多线程交互的场景:
有3个线程,让这三个线程交替打印abc abc,一共打印4次
这个场景要怎么实现呢?
首先,我们需要有3个线程,t1
,t2
,t3
这三个线程分别负责打印a
,b
,c
,所以,我们需要一个负责打印的方法。但是 当t1
打印完a
之后,不能马上再次打印a
,必须等t2
,t3
打印完毕后,才能继续打印,也就是说,这个打印的方法需要有个变量控制当前能不能进行打印,如果能打印,则打印,如果不能打印,就等待。printer
的代码如下
static class Printer{
private int current = 1;
private final Object lockObj = new Object();
Logger logger = LoggerFactory.getLogger(this.getClass());
public void print(String str,int signal,int nextSignal){
for (int i = 0; i < 4; i++) {
synchronized (lockObj){
while (current!=signal){
try {
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info(str);
current = nextSignal;
lockObj.notifyAll();
}
}
}
}
线程调用printer:
Printer printer = new Printer();
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
printer.print("a", 1, 2);
});
Thread t2 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
printer.print("b", 2, 3);
});
Thread t3 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
printer.print("c", 3, 1);
});
t1.start();
t2.start();
t3.start();
countDownLatch.countDown();
因为必须是abc,按照这个顺序打印,所以t1
必须是第一个执行的,也就是说printer
中默认的标志位得是t1
的标志位(1)。因为这三个线程是同时启动的,假如t3
先或得CPU,这时,t3
通过synchronized获取到锁,然后判断,当前t3
是否可以打印,如果t3
不能打印current!=signal
,那么就等待,阻塞当前线程。假如,这时,t1
或得了CPU,并且满足了打印的条件,那么t1
开始打印,打印完毕后,将标志位标记为t2
(2),同时,唤醒所有等待的线程。这时候,t3
也会被唤醒,被唤醒的线程会从wait
之后继续执行,但是,t3
可能还不满足打印条件,所以,这里是while (current!=signal)
程序的执行结果如下:
同时,这个printer
还有一个ReentrantLock
版本的,大体意思一样,只是使用了ReentrantLock
static class Printer{
private int current = 1;
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
Logger logger = LoggerFactory.getLogger(this.getClass());
public void print(String str,int signal,int nextSignal){
for (int i = 0; i < 4; i++) {
try {
lock.lock();
while (current!=signal){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info(str);
current = nextSignal;
condition.signalAll();
}finally {
lock.unlock();
}
}
}
}
使用ReentrantLock
时,一定要记得try finally
,在finally
中释放锁。
总结:
1、 但凡synchronized+wait+notify
,一定会用到while
,因为notify
(随机唤醒)/notifyALL
(唤醒所有)之后,可能唤醒的并不是真的可以执行的线程,需要再次判断是否满足继续执行的条件。所以就得用到while
。
2、对这个问题的理解。之前对这个问题的理解或者说对多线程的理解不到位,总是觉得无从下手。尤其是不理解printer
中for
到底循环的是什么,一直以为循环的是abc abc,但是,其实对于t1
来说,这个for
循环的结果是aaa,对于t2
来说,这个for
循环的结果是bbb。多线程的问题还是得站在线程的角度看,才能真正理解