AQS 最容易被忽略但最关键的一点是CAS 抢 state 只是“快路径”真正让高并发下系统还能跑稳的是失败线程的排队与阻塞唤醒机制。AQS 使用的是CLH 变体的 FIFO 双向队列。你必须记住的 3 句话面试直出线程获取失败会入队不是为了“排队直接发锁”而是为了把竞争变得有序可控。只有“head 的后继”才更有资格反复 tryAcquire其余线程会 park 减少空转。释放时通常只唤醒一个后继避免惊群。1. 先讲结论AQS 队列里存什么队列节点Node大致包含thread当前等待的线程prev/next前驱/后继waitStatus节点状态是否需要唤醒、是否取消等你至少要知道两个最常见状态细节不必死背但要会解释含义SIGNAL表示“我的后继需要被唤醒”前驱释放时会 unpark 后继CANCELLED线程超时/中断/放弃竞争后的取消节点需要被跳过关键理解队列不是“直接发放锁”的结构它只是提供秩序让“谁有资格再去尝试抢 state”更可控2. 入队为什么失败后要排队如果失败线程不排队而是所有线程都不停 CAS 抢 state会导致巨量的总线争用与 CPU 空转系统抖动明显吞吐不稳定延迟尖刺所以 AQS 的策略是抢不到 -进入队列只有“队列头的后继”才更有资格去重试3. 一个关键细节为什么要把前驱标记成SIGNAL你会看到 AQS 在决定 park 之前会先把“前驱节点”标记为SIGNAL语义是前驱释放时需要负责唤醒它的后继直觉理解这相当于建立一条责任链谁在我前面谁负责在释放时叫醒我。它避免了“释放时不知道叫醒谁”的混乱也减少了无意义唤醒。4. 出队与唤醒释放时为什么只唤醒一个独占锁释放时通常只唤醒一个后继线程因为同一时刻只允许一个线程成功唤醒多个会造成“惊群”大量线程醒来再失败再睡回去这也是为什么ReentrantLock的 signal 与 signalAll 需要谨慎。补充为什么 unpark 不是“立刻让线程拿到锁”unpark 只是在调度层面把线程从阻塞态唤醒真正能否成功仍要靠它醒来后再 tryAcquire 的 CAS 竞争5. 取消节点超时/中断为什么不会把队列“卡死”现实里线程可能会超时放弃被中断直接取消等待这会导致队列中出现CANCELLED节点。关键点AQS 在入队、自旋、唤醒时都会跳过取消节点并尝试修复前驱/后继指针目的只有一个保证队列还能向前推进唤醒链路不会断面试人话队列里可能有人“退出排队”但队列不会因此堵死会自动绕过这些退出者。6. park/unpark阻塞与唤醒的语义AQS 使用LockSupport.park/unpark典型流程线程入队后在合适时机 park挂起前驱释放锁时 unpark 后继唤醒注意点unpark可以先于park调用有“许可”语义因此更适合构建并发框架这点在面试里很加分Object.notify需要先 wait 才有意义LockSupport.unpark可以先发“许可”后续 park 会直接通过7. 公平性从哪来FIFO 前驱判断所谓公平锁并不是“队列里的人一定按顺序拿到锁”而是新来的线程不会插队在 AQS 语义下通常体现在公平锁获取时会先判断hasQueuedPredecessors()只要队列里有人排在前面就不走“直接 CAS 抢 state”的快路径而非公平锁先抢一次 state允许插队失败才入队8. 你需要能解释的一个细节为什么“头节点后继”才去重试队列中的线程并不是都在自旋。典型做法是只有当自己的前驱是 head才认为“轮到我”再去 CAS 尝试否则就 park这样可以把竞争范围从“所有线程”缩小为“极少数线程”。再补一个常见细节为什么要有 head 哑节点head 通常是一个“哑节点”不代表真实线程用来简化出队/唤醒逻辑“谁是 head 的后继”就是一个明确的资格判定点9. 线上排查怎么看出线程卡在 AQS 队列上现象接口 RT 飙升线程数上涨jstack 大量线程处于WAITING (parking)你在堆栈里常能看到java.util.concurrent.locks.LockSupport.parkjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireReentrantLock$NonfairSync/FairSync.lock判读如果很多线程卡在同一个锁的 acquire 上说明临界区过长或锁粒度过大进一步区分两种常见形态大量WAITING (parking)线程主要在排队等待锁竞争或临界区长CPU 很高 acquire 相关栈频繁出现可能存在更强的自旋/重试热点 state/CAS10. 自测清单你要能顺口讲出来QAQS 队列是干什么的A保存获取失败的线程提供 FIFO 排队与阻塞唤醒秩序避免无序竞争导致的抖动。Q公平锁为什么吞吐更低A因为禁止插队减少快路径命中更多线程会入队/唤醒调度开销更高。Q释放锁为什么通常唤醒一个线程A独占语义下同时只能一个成功唤醒多个会惊群。Q队列里有取消节点会怎样AAQS 会在遍历/唤醒时跳过取消节点保证队列还能向前推进否则容易出现“断链/唤醒不到人”的风险。Q为什么要把前驱标成SIGNAL才 parkA让前驱承担“释放时唤醒后继”的责任链避免唤醒丢失也避免释放时乱唤醒。QAQS 为什么要有 head 哑节点A简化出队与唤醒逻辑明确“head 的后继”这个资格位点让竞争可控。11. 30 秒背诵稿AQS 在获取失败时把线程包装成 Node 入 CLH FIFO 队列通过前驱/后继指针维护 FIFO 秩序只有 head 的后继才反复 tryAcquire其余线程会在前驱标记为 SIGNAL 后 park 阻塞避免空转。释放时通常 unpark 一个合适后继唤醒不等于“交锁”醒来仍需 CAS 再竞争。公平锁通过判断队列前驱来禁止插队非公平锁则先 CAS 抢一次以提高吞吐。
AQS 的 CLH 同步队列:入队/出队、park/unpark 与“公平性”从哪来
发布时间:2026/5/31 17:56:57
AQS 最容易被忽略但最关键的一点是CAS 抢 state 只是“快路径”真正让高并发下系统还能跑稳的是失败线程的排队与阻塞唤醒机制。AQS 使用的是CLH 变体的 FIFO 双向队列。你必须记住的 3 句话面试直出线程获取失败会入队不是为了“排队直接发锁”而是为了把竞争变得有序可控。只有“head 的后继”才更有资格反复 tryAcquire其余线程会 park 减少空转。释放时通常只唤醒一个后继避免惊群。1. 先讲结论AQS 队列里存什么队列节点Node大致包含thread当前等待的线程prev/next前驱/后继waitStatus节点状态是否需要唤醒、是否取消等你至少要知道两个最常见状态细节不必死背但要会解释含义SIGNAL表示“我的后继需要被唤醒”前驱释放时会 unpark 后继CANCELLED线程超时/中断/放弃竞争后的取消节点需要被跳过关键理解队列不是“直接发放锁”的结构它只是提供秩序让“谁有资格再去尝试抢 state”更可控2. 入队为什么失败后要排队如果失败线程不排队而是所有线程都不停 CAS 抢 state会导致巨量的总线争用与 CPU 空转系统抖动明显吞吐不稳定延迟尖刺所以 AQS 的策略是抢不到 -进入队列只有“队列头的后继”才更有资格去重试3. 一个关键细节为什么要把前驱标记成SIGNAL你会看到 AQS 在决定 park 之前会先把“前驱节点”标记为SIGNAL语义是前驱释放时需要负责唤醒它的后继直觉理解这相当于建立一条责任链谁在我前面谁负责在释放时叫醒我。它避免了“释放时不知道叫醒谁”的混乱也减少了无意义唤醒。4. 出队与唤醒释放时为什么只唤醒一个独占锁释放时通常只唤醒一个后继线程因为同一时刻只允许一个线程成功唤醒多个会造成“惊群”大量线程醒来再失败再睡回去这也是为什么ReentrantLock的 signal 与 signalAll 需要谨慎。补充为什么 unpark 不是“立刻让线程拿到锁”unpark 只是在调度层面把线程从阻塞态唤醒真正能否成功仍要靠它醒来后再 tryAcquire 的 CAS 竞争5. 取消节点超时/中断为什么不会把队列“卡死”现实里线程可能会超时放弃被中断直接取消等待这会导致队列中出现CANCELLED节点。关键点AQS 在入队、自旋、唤醒时都会跳过取消节点并尝试修复前驱/后继指针目的只有一个保证队列还能向前推进唤醒链路不会断面试人话队列里可能有人“退出排队”但队列不会因此堵死会自动绕过这些退出者。6. park/unpark阻塞与唤醒的语义AQS 使用LockSupport.park/unpark典型流程线程入队后在合适时机 park挂起前驱释放锁时 unpark 后继唤醒注意点unpark可以先于park调用有“许可”语义因此更适合构建并发框架这点在面试里很加分Object.notify需要先 wait 才有意义LockSupport.unpark可以先发“许可”后续 park 会直接通过7. 公平性从哪来FIFO 前驱判断所谓公平锁并不是“队列里的人一定按顺序拿到锁”而是新来的线程不会插队在 AQS 语义下通常体现在公平锁获取时会先判断hasQueuedPredecessors()只要队列里有人排在前面就不走“直接 CAS 抢 state”的快路径而非公平锁先抢一次 state允许插队失败才入队8. 你需要能解释的一个细节为什么“头节点后继”才去重试队列中的线程并不是都在自旋。典型做法是只有当自己的前驱是 head才认为“轮到我”再去 CAS 尝试否则就 park这样可以把竞争范围从“所有线程”缩小为“极少数线程”。再补一个常见细节为什么要有 head 哑节点head 通常是一个“哑节点”不代表真实线程用来简化出队/唤醒逻辑“谁是 head 的后继”就是一个明确的资格判定点9. 线上排查怎么看出线程卡在 AQS 队列上现象接口 RT 飙升线程数上涨jstack 大量线程处于WAITING (parking)你在堆栈里常能看到java.util.concurrent.locks.LockSupport.parkjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireReentrantLock$NonfairSync/FairSync.lock判读如果很多线程卡在同一个锁的 acquire 上说明临界区过长或锁粒度过大进一步区分两种常见形态大量WAITING (parking)线程主要在排队等待锁竞争或临界区长CPU 很高 acquire 相关栈频繁出现可能存在更强的自旋/重试热点 state/CAS10. 自测清单你要能顺口讲出来QAQS 队列是干什么的A保存获取失败的线程提供 FIFO 排队与阻塞唤醒秩序避免无序竞争导致的抖动。Q公平锁为什么吞吐更低A因为禁止插队减少快路径命中更多线程会入队/唤醒调度开销更高。Q释放锁为什么通常唤醒一个线程A独占语义下同时只能一个成功唤醒多个会惊群。Q队列里有取消节点会怎样AAQS 会在遍历/唤醒时跳过取消节点保证队列还能向前推进否则容易出现“断链/唤醒不到人”的风险。Q为什么要把前驱标成SIGNAL才 parkA让前驱承担“释放时唤醒后继”的责任链避免唤醒丢失也避免释放时乱唤醒。QAQS 为什么要有 head 哑节点A简化出队与唤醒逻辑明确“head 的后继”这个资格位点让竞争可控。11. 30 秒背诵稿AQS 在获取失败时把线程包装成 Node 入 CLH FIFO 队列通过前驱/后继指针维护 FIFO 秩序只有 head 的后继才反复 tryAcquire其余线程会在前驱标记为 SIGNAL 后 park 阻塞避免空转。释放时通常 unpark 一个合适后继唤醒不等于“交锁”醒来仍需 CAS 再竞争。公平锁通过判断队列前驱来禁止插队非公平锁则先 CAS 抢一次以提高吞吐。