并发编程—等待-通知
有上一篇文章我们知道,在破坏占用且等待条件的时候,如果两个资源有一个被占用后,用的是死循环的方式来循环等待,代码如下所示:
//循环的方式
while (!allocator.apply(this, tar)) ;
如果说apply()操作耗时非常短,而且并发冲突量不大时,可以使用这个方案。如果apply()操作非常耗时,或者并发冲突量非常大的时候,这种循环等待的方案就不适用了,因为这种场景下,可能要循环上万次才能获取到锁,相当耗CPU。
其实在这种场景下,做好的方案是:如果线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程需要的条件满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗CPU的问题。
Java中是通过 synchronized 关键字配合 wait()、notify()和notifyAll()这三个方法就能轻松实现。
如何使用synchronized实现互斥锁,想必你已经很熟悉了。如下图所示:
image在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java对象wait()方法就能满足这种需求。当调用了wait()方法后,当前线程就会被阻塞,并且进入右边的等待队列中,这个等待队列也是互斥锁的等待队列。线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。
当线程需要的条件满足是,Java中的notify()和notifyAll()方法,就会通知阻塞队列中的线程高速他条件曾经满足过。因为notify()和notifyAll()只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所有当线程执行的时候,可能有其他线程已经插队执行了,又是条件不满足了。
在使用 wait()、notify()和notifyAll()方法时要在synchronized代码块中,否则会报java.lang.IllegalMonitorStateException异常。
在这个等待-通知机制中,我们需要考虑一下四个要素:
- 互斥锁:等待-通知首先需要满足互斥
- 线程要求的条件
- 何时等待
- 何时通知
尽量使用notifyAll
因为notify()方法是随机地通知等待队列中的一个线程,而notifyAll()会通知等待队列中的所有线程。比如在多生产-多消费者模式中,如果使用notify()方法可能每次唤醒的都是 生产者或者消费者线程,这样可能会导致某一方始终得不到执行机会。
所以除非经过深思熟虑,否则尽量使用notifyAll()方法。
总结
等待-通知机制是一种非常普遍的线程间协作方式。Java 语言内置的 synchronized 配合 wait()、notify()、notifyAll() 这三个方法可以快速实现这种机制,但是它们的使用看上去还是有点复杂,所以你需要认真理解等待队列和 wait()、notify()、notifyAll() 的关系。最好用现实世界做个类比,这样有助于你的理解