在使用并发编程时,利用多线程来提高任务的执行效率,但是每个线程在执行时,都有一些先决条件需要被满足。例如生产者消费者模式下,消费者线程能够执行的先决条件,就是生产者产生了一个待消费的数据。
那么如果线程要求的条件,不满足时,循环等待是一种方案,循环间隔一段时间,再重新尝试验证条件是否满足,但是这样的循环等待,在某些场景下,可能会循环很多次,导致大量消耗 CPU 资源。
更好的方案,则是等待-通知机制。当线程要求的条件不满足时,主动进入等待状态,等线程等待的条件,再次被满足后,通知这个等待的线程重新执行。这就解决了 CPU 资源,因为循环而导致消耗的问题。
对标到 Synchronized 中,被 Synchronized 标记的代码块,被称为临界区,在同一时刻,只有一个线程能过获取 Synchronized 的互斥锁,进入临界区执行。
线程处于临界区时,一旦发现执行条件不满足时,则可以调用 wait()
或者wait(time_out)
方法,进入等待,例如消费者发现没有更多需要处理的数据,此时就调用 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()
主要是为了稳定,减少程序的复杂度,我们程序员,解决的是一系列工程问题,虽然有时候需要挑战一些性能的极限,但是大多数时候应该是以稳定且易读易维护为出发点实现功能。
但是在程序的世界中,永远没有绝对的银弹。不带场景去分析问题,都是耍流氓。
前面说 “除非深思
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
熟虑”,那什么场景下才可以用到深思熟虑?
其实只要满足三个条件即可:
- 线程进入等待队列的条件相同。