take()方法在队列为空时通过AQS的notEmpty条件变量阻塞:先释放锁,再入条件队列;被signal后转入同步队列重新争锁,全程原子、可中断且支持多条件精确唤醒。

BlockingQueue 的 take() 方法在队列为空时会阻塞,其核心依赖于 AQS(AbstractQueuedSynchronizer)中的条件变量 notEmpty。这个过程不是简单地调用 wait(),而是通过 AQS 的独占锁 + 条件队列协作完成挂起与唤醒——关键在于「先释放锁、再入条件队列、被 signal 后重新竞争锁」这一闭环。
notEmpty 条件变量的创建与归属
notEmpty 是一个 ConditionObject 实例,由 ReentrantLock 创建并绑定到其内部的 AQS 上。它不独立存在,必须依附于某个独占锁(即 BlockingQueue 实现中使用的 lock,如 ArrayBlockingQueue 中的 lock)。所有对 notEmpty.await() 和 notEmpty.signal() 的调用,都需在持有该锁的前提下进行。
- 每个 ConditionObject 维护自己的条件队列(FIFO),节点类型是
Node.CONDITION - 条件队列和 AQS 同步队列(sync queue)物理隔离,但逻辑联动:await 时节点从 sync queue 出队,进入 condition queue;signal 时节点从 condition queue 转移到 sync queue 尾部
-
notEmpty语义上表示“队列非空”,所以只有在成功出队后(即 take 真正拿到元素),才会触发notEmpty.signal()—— 这通常发生在put()或其它修改队列的操作里
take() 中 await 的完整挂起流程
以 ArrayBlockingQueue 为例,take() 执行路径为:lock.lockInterruptibly() → 检查队列是否为空 → 若空则调用 notEmpty.await() → 挂起当前线程。
- 调用
await()前,线程已持有 lock(即已进入 AQS 同步队列并获得锁) -
await()内部首先将当前线程构造成Node.CONDITION节点,插入到notEmpty的条件队列尾部 - 然后完全释放锁(调用
fullyRelease(node)),这会导致 AQS 的 state 归零,并唤醒同步队列中下一个等待者(如有) - 最后线程进入 WAITING 状态,不再参与 CPU 竞争,直到被
signal()唤醒或中断
signal() 如何触发线程重新竞争锁
当另一个线程执行 put() 并成功入队后,会调用 notEmpty.signal() —— 注意这不是唤醒任意线程,而是唤醒条件队列头节点。
-
signal()先将条件队列头节点(Node.CONDITION)从条件队列中移除 - 接着调用
enq(node)将其作为普通节点加入 AQS 同步队列尾部,并设置其 waitStatus 为 0 - 此时该节点尚未获得锁,只是获得了“排队资格”;它需要像其他同步队列节点一样,等待前驱节点释放锁后,才能尝试 acquire
- 一旦轮到它 acquire,会再次尝试获取锁(即 CAS 修改 state),成功则继续执行
take()剩余逻辑(取元素、返回)
为什么不能直接用 Object.wait()/notify()?
AQS 的条件变量解决了内置锁条件机制的几个硬伤:
- 支持多个独立条件队列(如 notEmpty 和 notFull),而 synchronized 只有一个 wait set,易导致信号错乱
- 精确控制唤醒目标(signal 而非 notifyAll),避免惊群效应
- 挂起前自动释放锁,唤醒后自动重入锁,整个过程原子且可中断(await 可响应 interrupt)
- 与 AQS 同步语义深度集成,能统一处理超时、中断、公平/非公平策略等
理解 notEmpty 的 await/signal 协作,本质是理解 AQS 如何把「等待某个业务条件成立」这一高层语义,拆解为「释放锁 → 进条件队列 → 被转移至同步队列 → 重新争锁」这一底层状态机。它不神秘,但每一步都紧扣锁与条件的分离与协同。










