当前位置: 首页 > 科技新闻 >

为什么 wait 必须在 synchronized 保护的同步代码中使

时间:2020-06-04 17:38来源:网络整理 浏览:
作者简介:徐隆曦,滴滴出行高级工程师。本文选自:拉勾教育专栏《Java 并发编程 78 讲》你好,我是徐隆曦,今天我们主要探讨一个问题​:​

作者简介:徐隆曦,滴滴出行高级工程师。

本文选自:拉勾教育专栏《Java 并发编程 78 讲》

你好,我是徐隆曦,今天我们主要探讨一个问题​:​​为什么 wait 必须在 synchronized 保护的同步代码中使用?

为什么 wait 必须在 synchronized 保护的同步代码中使用?

首先,我们来看第一个问题,为什么 wait 方法必须在 synchronized 保护的同步代码中使用?

我们先来看看 wait 方法的源码注释是怎么写的:

为什么 wait 必须在 synchronized 保护的同步代码中使用?

上面这段英文的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。那么设计成这样有什么好处呢?

本文选自:拉勾教育专栏《Java 并发编程 78 讲》见文末了解更多

为什么 wait 必须在 synchronized 保护的同步代码中使用?

我们逆向思考这个问题,如果不要求 wait 方法放在 synchronized 保护的同步代码中使用,而是可以随意调用,那么就有可能写出这样的代码:

为什么 wait 必须在 synchronized 保护的同步代码中使用?

在代码中可以看到有两个方法,give 方法负责往 buffer 中添加数据,添加完之后执行 notify 方法来唤醒之前等待的线程,而 take 方法负责检查整个 buffer 是否为空,如果为空就进入等待,如果不为空就取出一个数据,这是典型的生产者消费者的思想。

本文选自:拉勾教育专栏《Java 并发编程 78 讲》见文末了解更多

为什么 wait 必须在 synchronized 保护的同步代码中使用?

但是这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:

首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。为什么 wait 必须在 synchronized 保护的同步代码中使用?

虽然刚才消费者判断了 buffer.isEmpty 条件,但真正执行 wait 方法时,之前的buffer.isEmpty 的结果已经过期了,不再符合最新的场景了,因为这里的“判断-执行”不是一个原子操作,它在中间被打断了,是线程不安全的。

假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 give 方法内的 notify 的唤醒。

我们看到正是因为 wait 方法所在的 take 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作,那么此时整个程序就很容易出错。

为什么 wait 必须在 synchronized 保护的同步代码中使用?

我们把代码改写成源码注释所要求的被 synchronized 保护的同步代码块的形式,代码如下:

为什么 wait 必须在 synchronized 保护的同步代码中使用?

这样就可以确保 notify 方法永远不会在 buffer.isEmpty 和 wait 方法之间被调用,提升了程序的安全性。

另外,wait 方法会释放monitor锁,这也要求我们必须首先进入到 synchronized 内持有这把锁。

为什么 wait 必须在 synchronized 保护的同步代码中使用?

这里还存在一个“虚假唤醒”(spurious wakeup)的问题,线程可能在既没有被notify/notifyAll,也没有被中断或者超时的情况下被唤醒,这种唤醒是我们不希望看到的。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序依然需要保证在发生虚假唤醒的时候的正确性,所以就需要采用while循环的结构:

为什么 wait 必须在 synchronized 保护的同步代码中使用?

这样即便被虚假唤醒了,也会再次检查while里面的条件,如果不满足条件,就会继续wait,也就消除了虚假唤醒的风险。

好了,本次的内容讲完了,下一课时我将讲解“有哪几种实现生产者-消费者模式的方法?”记得按时来学习哦,下次见。

本文选自:拉勾教育专栏《Java 并发编程 78 讲》见文末了解更多

版权声明:本文版权归属拉勾教育及该专栏作者,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表,违者必究。

推荐内容