java.util.concurrent 包里几乎所有的锁和同步工具——ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock——底层都站在同一个基座上: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
2
3
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}

机制二:CLH 变体的双向等待队列

抢不到锁的线程被包装成 Node 节点,挂进一个 FIFO 双向链表(CLH 队列的变体)。队列有 headtail 指针,新来的线程 CAS 到队尾。每个节点有个 waitStatus,关键值是 SIGNAL(-1),表示"我的后继需要被我唤醒"。

1
2
3
4
5
   head                              tail
| |
[Node A]<->[Node B]<->[Node C]<->[Node D]
持有锁 等待 等待 刚入队
(dummy)

注意 head 通常是个"哨兵"节点(当前持有锁的线程或一个虚节点),真正在排队等待的从 head 的后继开始。

机制三:独占模式的 acquire 流程

ReentrantLock(默认非公平)为例,加锁走 AQS 的 acquire:

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

拆开看:

  1. tryAcquire(子类实现):试着 CAS 把 state 从 0 改成 1。非公平锁的实现是"上来先抢一把,不管队列里有没有人在等";如果当前线程已持有锁,就把 state+1 实现重入
  2. addWaiter:抢不到,就把当前线程包成 Node 用 CAS 挂到队尾。
  3. acquireQueued:在队列里自旋——只有当自己的前驱是 head(轮到自己了)时才再试 tryAcquire;否则把前驱的 waitStatus 置为 SIGNAL,然后调用 LockSupport.park(this) 真正阻塞,让出 CPU。

释放锁走 release:

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
if (tryRelease(arg)) { // state 减到 0
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}

tryRelease 把 state 减 1,减到 0 才算彻底释放(支撑重入),然后 unparkSuccessorLockSupport.unpark 精确唤醒队列里的下一个线程。

整个唤醒机制的精髓是 park/unpark 配对:不像 wait/notify 需要先持有监视器锁、且 notify 早于 wait 会丢信号,unpark 可以先于 park 调用(相当于发一个"许可"),线程后续 park 时直接拿走许可不阻塞。这让 AQS 的唤醒不会丢信号。

公平 vs 非公平:一个 if 的差别

公平锁和非公平锁的差别仅在 tryAcquire 里:公平锁在抢之前会先检查队列里有没有人排在自己前面(hasQueuedPredecessors()),有就乖乖排队;非公平锁直接 CAS 抢,抢不到才入队。

1
2
3
// 公平锁的判断多了这一步
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { ... }

工程权衡:非公平锁吞吐更高——因为减少了线程切换。考虑一个刚释放锁的线程和一个刚被唤醒的队列线程:非公平锁让前者直接插队拿到锁,避免了"唤醒队首线程 → 它再去抢"的上下文切换开销(这段空窗期叫"护航效应"的反面)。代价是可能造成队尾线程饥饿ReentrantLock 默认非公平正是这个取舍。synchronized 也是非公平的。

工程权衡与踩坑

  • park 不释放锁/不响应中断默认:LockSupport.park 被唤醒可能是 unpark、中断,甚至是虚假唤醒,所以 AQS 用自旋循环 + 状态判断兜底,被唤醒后还要再判断"是否真的轮到我"。
  • lock 必须配 try-finally:ReentrantLock 不像 synchronized 会自动释放,忘了 unlock() 就是永久死锁。lock() 调用要放在 try 之外或第一行,unlock() 放在 finally。
  • 重入次数要对称:重入了 N 次就得 unlock N 次,state 才能归零。
  • Condition 与队列分离:ReentrantLocknewCondition() 维护的是另一条"条件等待队列",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 判断区分公平与非公平。读懂这套模板方法,并发包对你就不再是黑盒。