本文主要是介绍notifyAll(),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一. 序
当做多线程同步时,等待-通知机制是我们比较常用的一种选择,而 Java 中,等待-通知机制有多种实现,我们接触最早也是最熟悉的,应该就是 Java 语言内置的 synchronized 配合 wait()/notify()/notifyAll() 这三个方法来实现。
如何利用 synchronized 实现等待-通知机制,我想大家都比较熟悉,就无需多说了。notify() 和 notifyAll() 都可以唤醒等待的线程,但是应该使用 notify() 还是 notifyAll() 就比较有争议了。
一种比较流行的说法就是,除非深思熟虑,否者尽量使用 notifyAll()。
我们今天就这个问题,来讨论一下这两个方法如何选择。
二. 等待-通知机制
2.1 什么是等待通知机制
在此之前,先来聊聊什么是等待-通知机制,以及它能解决什么问题?
在使用并发编程时,利用多线程来提高任务的执行效率,但是每个线程在执行时,都有一些先决条件需要被满足。例如生产者消费者模式下,消费者线程能够执行的先决条件,就是生产者产生了一个待消费的数据。
那么如果线程要求的条件,不满足时,循环等待是一种方案,循环间隔一段时间,再重新尝试验证条件是否满足,但是这样的循环等待,在某些场景下,可能会循环很多次,导致大量消耗 CPU 资源。
更好的方案,则是等待-通知机制。当线程要求的条件不满足时,主动进入等待状态,等线程等待的条件,再次被满足后,通知这个等待的线程重新执行。这就解决了 CPU 资源,因为循环而导致消耗的问题。
对标到 Synchronized 中,被 Synchronized 标记的代码块,被称为临界区,在同一时刻,只有一个线程能过获取 Synchronized 的互斥锁,进入临界区执行。
线程处于临界区时,一旦发现执行条件不满足时,则可以调用 wait() 或者 wait(long) 方法,进入等待,例如消费者发现没有更多需要处理的数据,此时就调用 wait() 方法,进入等待,等待生产者产生一条新的待消费数据。接下来如果生产者线程,产生了一个新的数据后,就需要唤醒之前等待的消费者线程,去处理这条数据。这里的唤醒操作,就是调用 notify() 或者 notifyAll() 方法。
可以看到,notify() 和 notifyAll() 的作用有些类似,都是去唤醒等待中的线程。但是也正如他们方法名所描述的,notify() 会"随机"唤醒一个等待线程,而 notifyAll() 会尝试唤醒所有的等待中的线程。
注意这里的唤醒,并不是真的唤醒去执行,实际上只是让处于等待的线程,有重新获取锁的争抢权,也就是说,哪怕此时有一百个线程处于等待状态,此时调用 notifyAll() 也只会有一个线程获取到锁,允许进入临界区执行。
这在底层中,其实是利用了两个等待队列来实现的,分别是入口队列(EntrySet)和等待队列(WaitSet)。
被 Synchronized 阻塞等待的线程,会进入入口队列,而当条件不满足时,主动调用 wait() 方法进入等待的线程,则会进入等待队列。在等待队列中的线程,如果不被唤醒,则永远没有锁的争抢权,无法获取锁也就无法被执行。
2.2 为什么说尽量使用 notifyAll?
终于进入主题了,就前面的描述,看似应该是使用 notify() 更好一些,因为即便我们通知了等待队列中,所有的线程,但同一时刻,也只有一个线程可以获取互斥锁,进入临界区执行,这么看来 notify() 会更高效一些。
但是这里埋下来一个风险,就是只使用 notify() 可能会导致某些线程,一直处于等待队列中,而永远不会被唤醒并获得执行权。
理想情况下,一次等待(wait)对应一次通知(notify),是非常完美的,但是实际场景下,可能做不到。
例如,在多生产者消费者模式下,待处理的数据队列只有一条数据了,理想场景下,消费者在处理掉一条数据后,理论上应该唤醒生产着再生产一条新的待消费数据。可是 notify() 是随机唤醒,也就是它可能会唤醒一个消费者线程,这个消费者线程,发现没有待处理的数据,此时条件不满足,又主动进入等待队列。
也正是因为如此,在并发编程中有个范式模板:
synchronized(this){while(条件不满足) {wait();}// ...}
这段代码,大家应该很熟悉,notify() 只能保证唤醒一个线程,但是不保证线程执行的时候,曾经的等待条件已经被满足了。为了保证可靠性,此处使用循环检测的方式,只有必要条件满足时,才继续执行。
正是因为 notify() 随机唤醒的特点,导致在多条件的情况下,会导致某些线程永远不会被通知到。稳妥的方式,是使用 notifyAll(),让等待中的线程,都有一次再执行的权利。
这也就是为什么说,除非深思熟虑,否则尽量使用 notifyAll()。
2.3 什么是深思熟虑?
使用 notifyAll() 主要是为了稳定,减少程序的复杂度,我们程序员,解决的是一系列工程问题,虽然有时候需要挑战一些性能的极限,但是大多数时候应该是以稳定且易读易维护为出发点实现功能。
但是在程序的世界中,永远没有绝对的银丹。不带场景去分析问题,都是耍流氓。
前面说 “除非深思熟虑”,那什么场景下才可以用到深思熟虑?
其实只要满足三个条件即可:
- 线程进入等待队列的条件相同。
- 在满足条件时,所有线程执行的逻辑相同。
- 当一个线程执行完(无论是否异常),必定唤醒一个线程。
怎么理解呢?
当所有的线程,执行相同的业务逻辑,那么触发等待的条件也必定相同,只要保证一个线程在退出的时候,必定调用 notify() 唤醒一个线程,那么它就可以只使用 notify() 方法。
生产者消费者模式,必定是不满足的,它把线程分为两个性质。但是这种情况下,使用 Lock&Condition 会更适合。
三. 小结时刻
到这里,我相信大家已经理解了 notify 和 notifyAll 的区别了,在多数场景下,推荐使用 notifyAll() 的主要原因就是因为稳定,毕竟我们大多数时候解决的是工程问题。
这篇关于notifyAll()的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!