java.util.concurrent 包里几乎所有的锁和同步工具——ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock——底层都站在同一个基座上:AQS(AbstractQueuedSynchronizer)。理解了 AQS,你就一次性理解了半个并发包。本文从 ReentrantLock 切入,拆开 AQS 的状态、队列与阻塞唤醒机制。
场景:自己实现一把锁要解决什么
假设让你手写一把锁,你至少要回答三个问题:用什么表示"锁被持有"?抢不到锁的线程放哪、怎么排队?线程怎么阻塞、又怎么被精确唤醒?AQS 就是把这三件事抽象成了一个可复用的模板。
机制一:state——一个 int 表达万物
AQS 内部有一个 volatile int state,这是整个同步器的"同步状态"。妙处在于它的语义由子类定义:
ReentrantLock:state=0 表示未锁,state=N 表示被某线程重入了 N 次。Semaphore:state 表示剩余许可数。CountDownLatch:state 表示还差几个 countDown。- 读写锁:把 state 这个 int 拆成高 16 位(读锁计数)和低 16 位(写锁计数)。
对 state 的所有修改都走 CAS,保证原子性:
1 | protected final boolean compareAndSetState(int expect, int update) { |
机制二:CLH 变体的双向等待队列
抢不到锁的线程被包装成 Node 节点,挂进一个 FIFO 双向链表(CLH 队列的变体)。队列有 head 和 tail 指针,新来的线程 CAS 到队尾。每个节点有个 waitStatus,关键值是 SIGNAL(-1),表示"我的后继需要被我唤醒"。
1 | head tail |
注意 head 通常是个"哨兵"节点(当前持有锁的线程或一个虚节点),真正在排队等待的从 head 的后继开始。
机制三:独占模式的 acquire 流程
以 ReentrantLock(默认非公平)为例,加锁走 AQS 的 acquire:
1 | public final void acquire(int arg) { |
拆开看:
tryAcquire(子类实现):试着 CAS 把 state 从 0 改成 1。非公平锁的实现是"上来先抢一把,不管队列里有没有人在等";如果当前线程已持有锁,就把 state+1 实现重入。addWaiter:抢不到,就把当前线程包成 Node 用 CAS 挂到队尾。acquireQueued:在队列里自旋——只有当自己的前驱是 head(轮到自己了)时才再试tryAcquire;否则把前驱的 waitStatus 置为 SIGNAL,然后调用LockSupport.park(this)真正阻塞,让出 CPU。
释放锁走 release:
1 | public final boolean release(int arg) { |
tryRelease 把 state 减 1,减到 0 才算彻底释放(支撑重入),然后 unparkSuccessor 用 LockSupport.unpark 精确唤醒队列里的下一个线程。
整个唤醒机制的精髓是 park/unpark 配对:不像 wait/notify 需要先持有监视器锁、且 notify 早于 wait 会丢信号,unpark 可以先于 park 调用(相当于发一个"许可"),线程后续 park 时直接拿走许可不阻塞。这让 AQS 的唤醒不会丢信号。
公平 vs 非公平:一个 if 的差别
公平锁和非公平锁的差别仅在 tryAcquire 里:公平锁在抢之前会先检查队列里有没有人排在自己前面(hasQueuedPredecessors()),有就乖乖排队;非公平锁直接 CAS 抢,抢不到才入队。
1 | // 公平锁的判断多了这一步 |
工程权衡:非公平锁吞吐更高——因为减少了线程切换。考虑一个刚释放锁的线程和一个刚被唤醒的队列线程:非公平锁让前者直接插队拿到锁,避免了"唤醒队首线程 → 它再去抢"的上下文切换开销(这段空窗期叫"护航效应"的反面)。代价是可能造成队尾线程饥饿。ReentrantLock 默认非公平正是这个取舍。synchronized 也是非公平的。
工程权衡与踩坑
- park 不释放锁/不响应中断默认:
LockSupport.park被唤醒可能是 unpark、中断,甚至是虚假唤醒,所以 AQS 用自旋循环 + 状态判断兜底,被唤醒后还要再判断"是否真的轮到我"。 - lock 必须配 try-finally:
ReentrantLock不像synchronized会自动释放,忘了unlock()就是永久死锁。lock()调用要放在 try 之外或第一行,unlock()放在 finally。 - 重入次数要对称:重入了 N 次就得 unlock N 次,state 才能归零。
- Condition 与队列分离:
ReentrantLock的newCondition()维护的是另一条"条件等待队列",await把节点从同步队列移到条件队列,signal再移回来。别和主同步队列混淆。
共享模式一瞥
AQS 还支持共享模式(acquireShared/releaseShared),允许多个线程同时获取。Semaphore 用它实现 N 个许可,CountDownLatch 用它实现"state 归零前所有 await 线程一起放行"。区别在于:独占模式释放时只唤醒一个后继,共享模式会向后传播唤醒,可能连续放行多个等待线程。
小结
AQS 把锁的三大要素抽象为:一个 volatile int state(同步状态)、一个 CLH 双向等待队列(谁在排队)、park/unpark(阻塞与精确唤醒)。子类只需实现 tryAcquire/tryRelease(独占)或 tryAcquireShared/tryReleaseShared(共享),就能拼出各种同步工具。ReentrantLock 通过 state 计数实现重入,通过 tryAcquire 里一个 hasQueuedPredecessors 判断区分公平与非公平。读懂这套模板方法,并发包对你就不再是黑盒。