put 和 take 必须用 Condition 而非 wait/notify,因为二者需独立等待「队列未满」和「队列非空」两个条件;wait/notify 只能绑定单个监视器,易导致错误唤醒或死锁,而 Condition 支持同一锁下多条件队列,实现精准唤醒。

put 和 take 为什么必须用 Condition 而不是 wait/notify
因为 wait() 和 notify() 只能绑定在单个对象监视器上,而 put 和 take 需要两个独立的等待条件:「队列未满」和「队列非空」。如果共用一个锁对象并混用 notifyAll(),就会唤醒错误类型的线程(比如 put 成功后唤醒另一个 put 线程),造成无效调度甚至死锁。
Condition 允许为同一把锁创建多个等待队列:notFull 和 notEmpty 就是典型例子。每个 await() 调用只挂入对应条件队列,signal() 也只唤醒该队列里的线程 —— 这是精准唤醒的前提。
-
ArrayBlockingQueue用一把ReentrantLock+ 两个Condition实现 -
LinkedBlockingQueue用两把锁(putLock/takeLock)+ 各自配套的Condition,进一步解耦生产/消费路径 - 手写时若误用
Object.wait(),又没配对while循环检查条件,极易触发虚假唤醒后直接操作空/满队列
put 方法阻塞时到底在等什么、谁来唤醒它
put() 在队列满(count == capacity)时调用 notFull.await(),当前线程释放锁、进入 notFull 条件队列挂起。它不会被 take() 的任意执行唤醒,而是严格依赖 take() 中的 notFull.signal() —— 且仅当 take() 真正腾出一个空位(即 count 减少后 )才会触发。
- 唤醒时机:消费者成功取出一个元素,且取出后
count仍小于容量 → 才调notFull.signal() - 不唤醒的情况:消费者取完后队列刚好为空(
count == 0),此时只唤醒notEmpty队列,notFull保持静默 - 注意
signal()不保证立即执行:被唤醒线程需重新竞争锁,拿到锁后仍要再次检查isFull(),防止因竞争或中断导致状态已变
take 方法被唤醒后为何还要 while 循环判断 isEmpty
take() 中的 while (isEmpty()) { notEmpty.await(); } 不是可选写法,是强制要求。原因有三:
- 虚假唤醒:JVM 或 OS 可能在无 signal 时唤醒线程,此时队列可能依然为空
- 多线程竞争:多个
take线程同时被signal()唤醒,但只有一个能成功取走元素,其余必须继续等 - 中断干扰:线程被中断后
await()抛异常退出,但逻辑不能跳过条件检查直接执行取操作
所以不能写成 if (isEmpty()) await() —— 一旦漏掉循环,就等于默认“唤醒即条件成立”,实际中会立刻抛 NullPointerException 或返回 null。
LinkedBlockingQueue 的双锁如何影响 Condition 使用
LinkedBlockingQueue 把 putLock 和 takeLock 分开,意味着 notFull 绑定在 putLock 上,notEmpty 绑定在 takeLock 上。这带来两个关键影响:
-
put()中唤醒notEmpty必须先获取takeLock,否则无法调用notEmpty.signal()—— 源码里能看到signalNotEmpty()方法内部加了takeLock.lock() - 双锁提升吞吐量:生产者和消费者几乎不争同一把锁,但代价是
count变成跨锁共享变量,必须用AtomicInteger保证读写原子性 - 调试时容易忽略:比如在
put()中打日志发现notEmpty.signal()没生效,其实是卡在了takeLock获取上,而非信号丢失
真正容易被忽略的是:Condition 的生命周期完全依附于锁。一旦锁被销毁或重置(比如某些自定义实现中反复 new ReentrantLock),其关联的 Condition 就失效了 —— 这类问题在线上环境极难复现,但会导致线程永久挂起。










