本文正在参加「Java主题月 – Java 刷题打卡」,详情查看活动链接
为啥 wait
方法必须得在 synchronized
保护的同步代码中使用?
wait
方法的源码注释如下:
# “wait method should always be used in a loop:
synchronized (obj) {
while (condition does not hold)
obj.wait();
... // Perform action appropriate to condition
}
# This method should only be called by a thread that is the owner of this object's monitor.”
复制代码
翻译下,即: wait
方法应在 synchronized
保护的 while
代码块中使用,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait
方法。在执行 wait
方法之前,必须先持有对象的 monitor
锁,即 synchronized
锁。
为什么这样设计?这样设计又有什么好处?
反向思考,如果不要求 wait
方法放在 synchronized
保护的同步代码中使用,而是可以随意调用,那么就有可能写出这样的代码,如下:
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void offer(String data) {
buffer.add(data);
// Since someone may be waiting in take
notify();
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
复制代码
在代码中有两个方法:
offer
方法负责往buffer
中添加数据,添加完之后执行notify
方法来唤醒之前等待的线程take
方法负责检查整个buffer
是否为空,如果为空就进入等待,如果不为空就取出一个数据。
但是这段代码并没有受 synchronized
保护,于是便有可能发生以下场景:
- 首先,消费者线程调用
take
方法并判断buffer.isEmpty
方法是否返回true
,若为true
代表buffer
是空的,则线程希望进入等待,但是在线程调用wait
方法之前,就被调度器暂停了,所以此时还没来得及执行wait
方法。- 此时生产者开始运行,执行了整个
offer
方法,它往buffer
中添加了数据,并执行了notify
方法,但notify
并没有任何效果,因为消费者线程的wait
方法没来得及执行,所以没有线程在等待被唤醒。- 此时,刚才被调度器暂停的消费者线程回来继续执行
wait
方法并进入了等待。
把代码改写成源码注释所要求的被 synchronized
保护的同步代码块的形式,代码如下:
public void offer(String data) {
synchronized (this) {
buffer.add(data);
notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
复制代码
这样就可以确保 notify
方法永远不会在 buffer.isEmpty
和 wait
方法之间被调用,提升了程序的安全性。
另外,wait
方法会释放 monitor
锁,这也要求必须首先进入到 synchronized
内持有这把锁。
这里还存在一个“虚假唤醒”(spurious wakeup
)的问题,线程可能在既没有被 notify/notifyAll
,也没有被中断或者超时的情况下被唤醒,这种唤醒是不希望看到的。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序依然需要保证在发生虚假唤醒的时候的正确性,所以就需要采用 while
循环的结构。
while (condition does not hold)
obj.wait();
复制代码
这样即便被虚假唤醒了,也会再次检查 while
里面的条件,如果不满足条件,就会继续 wait
,也就消除了虚假唤醒的风险。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END