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/CAS)
10. 自测清单(你要能顺口讲出来)
-
Q:AQS 队列是干什么的?
- A:保存获取失败的线程,提供 FIFO 排队与阻塞唤醒秩序,避免无序竞争导致的抖动。
-
Q:公平锁为什么吞吐更低?
- A:因为禁止插队,减少快路径命中,更多线程会入队/唤醒,调度开销更高。
-
Q:释放锁为什么通常唤醒一个线程?
- A:独占语义下同时只能一个成功,唤醒多个会惊群。
-
Q:队列里有取消节点会怎样?
- A:AQS 会在遍历/唤醒时跳过取消节点,保证队列还能向前推进(否则容易出现"断链/唤醒不到人"的风险)。
-
Q:为什么要把前驱标成
SIGNAL才 park?- A:让前驱承担"释放时唤醒后继"的责任链,避免唤醒丢失,也避免释放时乱唤醒。
-
Q:AQS 为什么要有 head 哑节点?
- A:简化出队与唤醒逻辑,明确"head 的后继"这个资格位点,让竞争可控。
11. 30 秒背诵稿
AQS 在获取失败时把线程包装成 Node 入 CLH FIFO 队列,通过前驱/后继指针维护 FIFO 秩序;只有 head 的后继才反复 tryAcquire,其余线程会在前驱标记为 SIGNAL 后 park 阻塞,避免空转。释放时通常 unpark 一个合适后继,唤醒不等于"交锁",醒来仍需 CAS 再竞争。公平锁通过判断队列前驱来禁止插队,非公平锁则先 CAS 抢一次以提高吞吐。