跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

如何通过 AQS 的 ConditionObject 源码理解线程在等待队列与同步队列间的转移

await()时线程先通过fullyRelease()彻底释放锁并退出同步队列,再以CONDITION状态加入Condition等待队列;signal()则将等待队列头节点CAS修改状态后插入同步队列并唤醒。 await() 时线程怎么从同步队列“消失”,又进到等待队列 调用
await()
前,线程一定已持有锁(即已在 AQS 同步队列中成功 acquire),否则会抛
IllegalMonitorStateException
。真正释放锁的动作发生在
fullyRelease(node)
—— 它把当前线程持有的全部 state(比如可重入次数)一次性归零,并唤醒同步队列中下一个阻塞节点(如果存在)。此时线程已完全退出同步队列,也不再持有锁。 接着,该线程被封装成一个
Node
,以
Node.CONDITION
状态插入到 ConditionObject 的单向链表尾部(
addConditionWaiter()
),这个链表就是「等待队列」。注意:插入过程不需 CAS,因为调用
await()
必须在 lock 持有状态下执行,天然串行。 常见错误现象:
IllegalMonitorStateException
—— 忘了在
lock.lock()
之后调用
await()
关键点:
fullyRelease()
是彻底释放,不是“暂时让出”,所以其他线程能立刻抢到锁 性能影响:频繁 await/signal 会引发大量节点在两个队列间迁移,增加 CAS 和链表操作开销 signal() 怎么把节点从等待队列“搬”回同步队列
signal()
不唤醒线程本身,只做一件事:把等待队列头节点(
firstWaiter
)用 CAS 改为
0
状态(即脱离 CONDITION),然后调用
enq(node)
把它插进 AQS 同步队列尾部。这个过程由
transferForSignal()
完成。 一旦入队成功,就调用
LockSupport.unpark(node.thread)
唤醒线程。被唤醒的线程会从
LockSupport.park(this)
返回,接着执行
isOnSyncQueue(node)
判断——此时它已在同步队列中,循环退出,进入
acquireQueued()
竞争锁。 容易踩的坑:
signal()
调用后线程不会立即执行,仍要排队等锁;若此时锁被其他线程抢先获取,该线程继续阻塞在同步队列中 参数差异:
signal()
只搬头节点,
signalAll()
遍历整个等待队列,逐个
transferForSignal()
虚假唤醒防护:即使没被 signal,线程也可能因系统信号被 park 唤醒,所以
await()
外层必须用
while
而非
if
检查条件 中断发生时,节点转移逻辑会绕过 signal 直接进同步队列 线程在
await()
中被中断,
checkInterruptWhileWaiting()
会返回非 0 值(如
THROW_IE
或
REINTERRUPT
),并立即跳出等待循环。随后直接调用
acquireQueued(node, savedState)
—— 这个方法本质就是让该节点像普通竞争者一样进同步队列排队拿锁。 也就是说:**中断 ≈ 隐式 signal**,但不经过等待队列头部,而是由当前线程自己触发转移。这也解释了为什么中断后仍需重新 acquire:它没被 signal,锁资源还被别人占着。 典型错误写法:在
await()
外层 catch
InterruptedException
后直接 return,导致锁未重新获取、资源处于不一致状态 兼容性注意:
awaitUninterruptibly()
会忽略中断,直到被 signal 才返回,内部不调用
checkInterruptWhileWaiting()
调试线索:如果发现线程在没调用
signal()
的情况下突然开始竞争锁,优先检查是否被中断 多个 Condition 共享一把锁,但等待队列完全隔离 每个
ConditionObject
实例都有独立的
firstWaiter
/
lastWaiter
,互不干扰。比如
ReentrantLock
的
newCondition()
每次都 new 一个新对象,所以
c1.await()
和
c2.await()
的线程分别在不同链表里。 这种设计让「多条件等待」成为可能:生产者只
signal(notFull)
,消费者只
signal(notEmpty)
,彼此不会误唤醒。而 Object 的
wait()/notify()
只有一个隐式队列,只能靠
notifyAll()
撑场面。 实际陷阱:误把不同 Condition 的
signal()
当作通用唤醒,结果目标线程根本不在那个等待队列里,永远等下去 内存开销:每个 Condition 对象本身很轻量,但每个等待中的线程都会持有一个
Node
,长期堆积会增大队列长度 调试建议:用 jstack 查线程堆栈时,看到
parking to wait for <0x...>
只能说明在同步队列;要看是否在等待队列,得结合业务逻辑和 Condition 使用位置判断 真正难啃的是节点状态切换的原子性边界:CONDITION → 0 的 CAS 成功,不代表 enq 就一定成功;enq 成功,也不代表 unpark 就立刻生效。这些间隙正是并发复杂性的根源,也是你读源码时最该盯住的几行。

相关文章