2004年,Doug Lea在POC CSJP Workshop上发表了一篇题为《The java.util.concurrent Synchronizer Framework》的论文。这篇论文的核心贡献是提出了一个统一的同步器框架——AbstractQueuedSynchronizer(AQS),它用一个int类型的state变量和一个FIFO队列,解决了Java并发编程中几乎所有同步原语的实现问题。
这个设计的结果是,JDK中超过一半的同步器都基于AQS实现:ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock、ThreadPoolExecutor的Worker、甚至FutureTask的早期版本。一个抽象类何以支撑起如此庞大的技术体系?答案藏在对"同步"本质的深刻洞察中。
同步器的本质:state与队列
Doug Lea在论文中指出,几乎所有同步器都遵循相同的模式:
acquire操作:
while (同步状态不允许获取) {
将当前线程加入队列(如果尚未入队);
阻塞当前线程;
}
将当前线程从队列中移除;
release操作:
更新同步状态;
if (新状态允许其他线程获取) {
唤醒一个或多个队列中的线程;
}
这个模式揭示了同步器的两个核心组件:同步状态的管理和线程的排队与唤醒。AQS的设计哲学是将这两个组件的通用逻辑抽取出来,形成框架;而将具体的状态判断逻辑留给子类实现。
state:一个变量的千变万化
AQS使用一个volatile修饰的int变量表示同步状态:
private volatile int state;
这个看似简单的设计却蕴含着巨大的灵活性:
- ReentrantLock:state=0表示未锁定,state>0表示锁定次数(可重入)
- CountDownLatch:state表示剩余计数,countDown()将其减1,await()等待state=0
- Semaphore:state表示可用许可数量,acquire()减1,release()加1
- ReentrantReadWriteLock:state的高16位表示读锁计数,低16位表示写锁计数
为什么选择int而非long或boolean?Doug Lea在论文中解释:J2SE 1.5时代,许多平台的64位CAS操作仍需通过内部锁模拟,性能不佳。32位足以满足大多数场景——唯一的例外是CyclicBarrier,它最终选择了基于锁的实现。
对state的访问通过三个final方法:
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
CAS操作是整个框架的性能基石。在高并发场景下,CAS避免了锁的开销,使得无竞争的获取操作接近于直接内存访问的性能。
CLH队列:从自旋到阻塞的进化
当线程获取资源失败时,需要进入队列等待。AQS选择了CLH(Craig, Landin, and Hagersten)队列锁的变体作为等待队列的实现。
原始CLH队列
CLH队列最初是为自旋锁设计的。其核心思想是:每个等待线程持有一个节点,节点包含一个状态字段;线程自旋检查前驱节点的状态,而非竞争共享变量。
head tail
↓ ↓
+-------+ +-------+ +-------+
| Node |───→| Node |───→| Node |
|status | |status | |status |
+-------+ +-------+ +-------+
↑ ↑ ↑
Thread A Thread B Thread C
(已获取锁) (等待A释放) (等待B获取后释放)
CLH队列的优势在于:入队和出队是O(1)操作,无锁且无阻塞;状态存储分散在各节点,减少了内存竞争。但它有一个致命缺陷:只适合自旋等待。
AQS的改进:双向队列与阻塞机制
Doug Lea对CLH队列做了两项关键改进:
第一,由单向队列改为双向队列。原始CLH队列中,节点只保存前驱指针。但对于阻塞式同步器,释放锁的线程需要主动唤醒后继节点——没有后继指针就无法高效找到唤醒目标。
第二,引入阻塞机制替代纯自旋。纯自旋在高竞争下会浪费大量CPU周期。AQS的策略是:先短暂自旋尝试,失败后调用LockSupport.park()阻塞线程。
改进后的队列结构:

图片来源: JavaGuide - AQS详解
每个Node节点包含:
| 字段 | 说明 |
|---|---|
| thread | 封装的线程引用 |
| waitStatus | 节点状态(SIGNAL/CONDITION/CANCELLED/PROPAGATE/0) |
| prev | 前驱节点 |
| next | 后继节点 |
| nextWaiter | 条件队列中的后继节点 |
waitStatus状态机
waitStatus字段是一个精心设计的状态机:
| 状态 | 值 | 含义 |
|---|---|---|
| 0 | 新节点的初始状态 | |
| SIGNAL | -1 | 后继节点需要被唤醒 |
| CANCELLED | 1 | 线程因中断或超时取消等待 |
| CONDITION | -2 | 节点在条件队列中等待 |
| PROPAGATE | -3 | 共享模式下传播唤醒信号 |
状态流转的核心是SIGNAL:新节点入队时,会将其前驱节点的状态从0改为SIGNAL,表示"我需要你唤醒"。当节点释放资源时,检查自己是否为SIGNAL状态,如果是,则唤醒后继节点并将状态重置为0。
独占模式:一次只有一个线程
独占模式是最直观的同步方式,同一时刻只有一个线程能获取资源。以ReentrantLock为例,其获取锁的流程:
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程分解:
1. tryAcquire:尝试获取
这是模板方法,由子类实现。非公平锁的实现:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// CAS竞争获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入:state累加
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
2. addWaiter:入队等待
获取失败后,将当前线程封装为Node节点加入队列尾部:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // CAS失败,自旋重试
return node;
}
3. acquireQueued:队列中轮转
节点入队后,进入一个循环:如果前驱是头节点,则尝试获取;否则检查是否应该阻塞。
final boolean acquireQueued(final Node node, int arg) {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,成为新的头节点
p.next = null;
return false;
}
// 检查是否应该阻塞,并处理中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
}

图片来源: JavaGuide - AQS详解
释放锁的过程相对简单:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor从队列尾部向前遍历,找到第一个未取消的节点进行唤醒。为什么从尾部向前遍历?因为节点入队时,prev指针的设置早于next指针——从前往后遍历可能错过刚入队但next指针尚未设置的节点。
公平与非公平:吞吐量与公平性的权衡
ReentrantLock支持公平锁和非公平锁两种模式。它们的区别仅在于tryAcquire的实现:
非公平锁:直接尝试CAS获取,失败后才检查队列
公平锁:先检查队列中是否有等待更久的线程
// 公平锁的tryAcquire
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
hasQueuedPredecessors()检查队列中是否存在有效节点。这个简单的判断带来了显著的性能差异。
Doug Lea在论文中给出了基准测试数据:在256个线程的极端竞争下,非公平锁的吞吐量是公平锁的10-100倍。原因在于:当持有锁的线程释放时,唤醒队列中的线程需要时间(操作系统调度开销);在这段时间内,新来的线程可以"插队"获取锁,避免了锁资源的空闲。

图片来源: 美团技术团队 - AQS原理及应用
这就是"barging"策略:牺牲绝对的公平,换取更高的吞吐量。但公平锁仍有其用武之地——当锁持有时间较长、竞争不那么激烈时,公平锁可以避免某些线程长期饥饿。
共享模式:多个线程的并发访问
共享模式允许多个线程同时获取资源。以Semaphore为例,state表示可用许可数:
// Semaphore的非公平获取
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining; // 返回剩余资源数
}
}
共享模式的核心复杂性在于传播唤醒:当一个线程释放资源后,可能需要唤醒多个后继节点。这引出了PROPAGATE状态——它解决了并发场景下可能出现的唤醒丢失问题。
考虑这个场景:2个资源,4个线程。T3、T4持有资源,T1、T2在队列等待。T3释放时唤醒T1,T1获取成功但还未完成setHead操作;此时T4也释放,发现head状态为0(T1还没来得及设置前驱状态),按照正常逻辑不会唤醒。如果没有PROPAGATE状态,T2就会永远等待。
PROPAGATE的设计确保:即使head状态被重置为0,也能通过状态判断需要继续传播唤醒。
Condition:条件变量的实现
AQS内部的ConditionObject类实现了条件变量语义。每个Condition维护一个独立的条件队列:
await():释放锁 → 加入条件队列 → 阻塞等待 → 被signal后加入同步队列 → 重新获取锁
signal():将条件队列头节点转移到同步队列
与Object.wait/notify不同,一个Lock可以关联多个Condition,实现更精细的线程协调。例如,生产者-消费者模型中可以使用两个Condition分别管理"缓冲区满"和"缓冲区空"的等待队列。
模板方法模式的精妙应用
AQS的设计是模板方法模式的经典应用。框架定义了acquire/release的骨架,子类只需实现以下钩子方法:
| 方法 | 说明 |
|---|---|
| tryAcquire(int) | 独占模式获取 |
| tryRelease(int) | 独占模式释放 |
| tryAcquireShared(int) | 共享模式获取 |
| tryReleaseShared(int) | 共享模式释放 |
| isHeldExclusively() | 是否独占持有 |
这种设计使得实现一个新的同步器变得异常简单。以下是一个最简Mutex的实现:
class Mutex {
private static class Sync extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
十几行代码就实现了一个可用的互斥锁,底层自动获得队列管理、中断支持、超时机制等特性。
性能考量
Doug Lea在论文中给出了详细的性能测试数据:
无竞争开销(单线程,纳秒):
| 锁类型 | 开销 |
|---|---|
| synchronized | 58-189ns |
| Mutex (AQS) | 71-137ns |
| ReentrantLock | 81-140ns |
| Fair ReentrantLock | 108-139ns |
高竞争开销(256线程,纳秒):
| 锁类型 | 开销 |
|---|---|
| synchronized | 930-5214ns |
| Mutex (AQS) | 798-667ns |
| ReentrantLock | 843-188ns |
| Fair ReentrantLock | 14967-15328ns |
数据说明:非公平的AQS实现在高竞争下比synchronized快约一个数量级,而公平锁则慢一个数量级。这验证了barging策略的有效性。
结语
AQS的设计体现了一种深刻的工程智慧:将不变的部分固化在框架中,将变化的部分留给扩展者。一个int变量承载状态,一个队列管理等待,五个方法定义语义——这些简单的元素组合出了Java并发包中最丰富的同步原语生态。
Doug Lea在论文结尾写道:这个框架的成功在于它提供了足够高效的基础支持,使得"几乎从不需要从头实现同步器"。二十年后,这个判断依然成立。
参考资料
- Lea, D. (2004). The java.util.concurrent Synchronizer Framework. PODC CSJP Workshop.
- Craig, T. S. (1993). Building FIFO and priority-queueing spin locks from atomic swap. Technical Report TR 93-02-02, University of Washington.
- Magnusson, P., Landin, A., & Hagersten, E. (1994). Queue locks on cache coherent multiprocessors. 8th Int’l. Parallel Processing Symposium.
- Mellor-Crummey, J. M., & Scott, M. L. (1991). Algorithms for scalable synchronization on shared-memory multiprocessors. ACM Trans. on Computer Systems.
- Goetz, B., et al. (2006). Java Concurrency in Practice. Addison-Wesley.
- 美团技术团队. (2019). 从ReentrantLock的实现看AQS的原理及应用.
- JavaGuide. (2026). AQS详解.