Java并发:环形缓冲区两种实现中额外同步块的作用疑问
首先得明确Java里wait()/notifyAll()的硬性规则:调用某个对象的wait/notify方法,必须先持有该对象的锁;而且调用wait的线程会进入该对象的等待队列,只有持有同一对象锁的线程调用notifyAll,才能唤醒这个队列里的线程。
回到你的双锁环形缓冲区场景:
- 当缓冲区满时,添加线程会持有
addLock并调用addLock.wait(),进入addLock的等待队列 - 当缓冲区空时,移除线程会持有
removeLock并调用removeLock.wait(),进入removeLock的等待队列
现在拆解两个额外同步块的作用:
add方法里的
synchronized(removeLock)同步块
当add线程成功添加元素后,缓冲区从空转为非空——此时可能有一批移除线程因为之前缓冲区空,正卡在removeLock的等待队列里。要唤醒这些线程,必须调用removeLock.notifyAll(),但根据规则,调用这个方法必须先持有removeLock的锁。这个同步块的唯一作用就是临时获取removeLock的锁,合法地唤醒等待移除的线程。remove方法里的
synchronized(addLock)同步块
同理,当remove线程成功移除元素后,缓冲区从满转为非满——此时可能有添加线程因为之前缓冲区满,正卡在addLock的等待队列里。要唤醒这些线程,必须持有addLock的锁才能调用addLock.notifyAll(),这个同步块就是用来获取锁并执行唤醒操作的。
为什么不能用当前持有的锁来唤醒?因为等待的线程不在当前锁的队列里。比如add线程持有的addLock,它的等待队列里是等待添加的线程,而我们需要唤醒的是等待移除的线程——这些线程在removeLock的队列里,必须用removeLock来触发唤醒。
对比单锁(同步方法)的实现:所有线程都挤在同一个锁的队列里,notifyAll会唤醒所有等待线程,不仅会出现不必要的无效唤醒(比如唤醒了等待添加的线程但缓冲区还是满的),还会导致同一时间只能有一个线程操作,并发度极低。双锁方案的跨锁唤醒,既保证了添加和移除操作可以并行执行,又能精准唤醒真正需要被唤醒的线程,大幅提升了并发效率。
内容的提问来源于stack exchange,提问作者FaheDevvs

