Object::wait 最佳实践
Context
在 Object::wait
的方法注释中有这样一句:
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
1
2
3
4
5 synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
大意是 Object::wait
的最佳实践是放在循环体中使用,循环体之后是获取锁资源后真实执行的同步代码。
与此同时,我们在网上找到好多使用 if
代替 while
相关的 wait
代码,比如 java JUC 中 Object里wait()、notify() 实现原理及实战讲解、Java 多线程编程之:notify 和 wait 用法。当然,也有讨论为什么 wait 要放在 loop 中使用的文章:论Object.wait()要放到while循环里。文章都能看懂,但以前两篇文章为例,用 if
好像也没什么问题。
抛出问题
接下来,以下题为例讨论一下:
启动两个线程, 一个输出 1,3,5,7…99,另一个输出 2,4,6,8…100, 最后 STDOUT 中按序输出 1,2,3,4,5…100。
解题
题目中的题眼在于『按序输出 1,2,3,4,5…100』中的『按序』。如果单单是启动两个线程,一个输出奇数,一个输出偶数,很简单。只要控制好打印条件i%2
即可,但输出顺序无法保证,有可能得到
1 3 5 2 4 7…
所以需要引入互斥资源,保证交替打印。以下是我给出的第一份解法:👇🏻
1 | // 独占式对象资源 |
代码中在执行 wait
语句时使用的是 if
,最后输出的结果没啥问题。
我们将for
循环体中的逻辑改成范式试试看:
1 | // 引入公共变量 |
改造后,运行结果与原来无异。以上👆🏻面的改造为基础,单纯把 while
改造成 if
对结果也是没影响的。为什么呢?
因为只有两个线程,且每次 i++
都会满足另外一个线程中同步代码块的条件,完全不存在 虚假唤醒 !此时,我们需要像魏成一样,引入第三个球体。
引入第三个变量
此时,题目由原来的两个线程变成三个线程,同时启动,交替打印t1:1 t2:2 t3:3 t4:4 t5:5 t6:6… 即,3*+1 3*+2 3*+3
1 | public static Object r1 = new Object(); |
以上我们使用的是 if
,结果如下:
t1 get r1. i= 1
t1: 1
t1 after notify i: 1
t1 before wait. // t1 阻塞,进入monitor WAIT_SET 条件等待队列
t2 get r1. i= 2
t2: 2
t2 after notify i: 2 // t2 唤醒所有线程,t1 进入 ENTRY_SET 入口等待队列
t2 before wait. // t2 阻塞
t1 waited. // t1获取独占锁,但此时并不满足 i%3 != 1条件,不应向下执行。 ❌
t1: 3 // 我们的期望是 t3 输出 3 ❌
t1 after notify i: 3
t1: 4
t1 after notify i: 4
t1 before wait.
此时将上述 wait
代码块中的 if
改为 while
可完美解决问题。
可能有同学说了,那我把三个循环都改成这样,成不成啊?👇🏻
1 | for (; i<= 100; ++i){ |
那肯定不成啊。能保证三个线程依次获取锁资源么?妥妥不能,为啥?synchronized
是非公平锁喔~最后输出的没准儿是这样:
t1: 1
t1: 4
t1: 7
t3: 12
t1: 13
t2: 14
最终的是,wait
代码放最后,打印结束之后谁来唤醒呢?最后三个线程都一直处于阻塞状态,服务器要崩…
扩展:使用 ReentrantLock 和 Condition 该如何实现呢?
1 | ReentrantLock lock = new ReentrantLock(); |
或者使用 signal
方法:
1 | ReentrantLock lock = new ReentrantLock(); |