在 Java 并发编程的世界里,AbstractQueuedSynchronizer(简称 AQS)是一座隐藏的基石。从ReentrantLock到CountDownLatch,从Semaphore到ThreadPoolExecutor,诸多并发工具的实现都依赖于 AQS 的核心机制。AQS 通过巧妙的队列管理和状态控制,为开发者提供了构建同步器的通用框架。本文将深入 AQS 的内部世界,解析其核心结构、同步队列的运作机制及状态控制逻辑,揭开 Java 并发工具的底层奥秘。
一、AQS 的设计定位:同步器的通用骨架
AQS 是一个抽象类,其设计目标是为各种同步器提供基础框架。它封装了同步状态的管理、线程的排队等待、唤醒等核心操作,开发者只需重写少量方法即可实现自定义同步器。
1.1 核心设计思想
AQS 的核心思想可以概括为 **“状态控制 + 队列管理”**:
- 状态控制:通过一个 volatile 修饰的 int 变量(state)表示同步状态,子类通过getState()、setState()、compareAndSetState()等方法操作状态;
- 队列管理:当线程获取同步状态失败时,AQS 会将线程封装为节点并加入同步队列,等待被唤醒后重新尝试获取状态。
这种设计将 “实现同步” 的共性逻辑(如排队、唤醒)与 “判断是否允许访问” 的个性逻辑(如锁的获取条件)分离,极大简化了同步器的实现。
1.2 核心方法与模板模式
AQS 通过模板模式定义了同步操作的骨架,子类需重写以下关键方法(默认抛出UnsupportedOperationException):
方法 |
功能描述 |
protected boolean tryAcquire(int arg) |
独占式获取同步状态,返回true表示成功 |
protected boolean tryRelease(int arg) |
独占式释放同步状态,返回true表示成功 |
protected int tryAcquireShared(int arg) |
共享式获取同步状态,返回值≥0 表示成功 |
protected boolean tryReleaseShared(int arg) |
共享式释放同步状态,返回true表示后续线程可获取 |
protected boolean isHeldExclusively() |
判断当前线程是否独占同步状态 |
AQS 提供的模板方法(如acquire()、release()、acquireShared()等)会调用上述方法,实现完整的同步逻辑。例如,ReentrantLock通过重写tryAcquire()和tryRelease()实现独占锁,CountDownLatch通过重写tryAcquireShared()和tryReleaseShared()实现共享同步。
二、AQS 的内部结构:状态与队列的协作
AQS 的内部结构主要包括同步状态(state) 和同步队列(CLH 队列) 两部分,二者的协作是实现同步的关键。
2.1 同步状态(state)的设计
AQS 用一个 volatile 变量state存储同步状态,其含义由子类定义:
- 在ReentrantLock中,state表示锁的重入次数(0 表示未锁定,≥1 表示已锁定);
- 在Semaphore中,state表示可用许可的数量;
- 在CountDownLatch中,state表示计数器的初始值。
状态操作的线程安全性:AQS 通过Unsafe类的 CAS 操作保证state的原子性,例如compareAndSetState(int expect, int update)方法会原子性地将state从expect更新为update,失败则返回false。
// AQS中state的定义与操作
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// 调用Unsafe的CAS操作
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2 同步队列(CLH 队列)的结构
当线程获取同步状态失败时,AQS 会将线程封装为节点(Node) 并加入同步队列。该队列是一个双向链表,基于 CLH(Craig, Landin, and Hagersten)锁队列改进而来,特点是FIFO(先进先出) 和自旋等待。
2.2.1 节点(Node)的结构
每个节点包含以下核心字段:
static final class Node {
// 节点状态:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0(初始)
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 关联的线程
volatile Thread thread;
// 条件队列中的后继节点(用于Condition)
Node nextWaiter;
}
节点状态(waitStatus)的含义:
- CANCELLED(1):节点已取消(如超时或被中断),不再参与竞争;
- SIGNAL(-1):后继节点需要被唤醒,当前节点释放锁时需唤醒后继;
- CONDITION(-2):节点处于条件队列中,等待被唤醒;
- PROPAGATE(-3):共享模式下,状态需向后传播(如 CountDownLatch);
- 0:初始状态,无特殊含义。
2.2.2 队列的头节点与尾节点
AQS 通过head和tail指针维护同步队列:
- head:头节点,表示当前持有锁的线程(或已成功获取状态的线程);
- tail:尾节点,新节点会被加入到尾部。
// AQS中队列的头、尾指针
private transient volatile Node head;
private transient volatile Node tail;
队列初始化:队列初始时为空(head和tail均为null)。当第一个线程获取状态失败时,会创建头节点和尾节点(头节点为哨兵节点,不关联线程),后续节点通过 CAS 操作加入队列尾部。
三、独占式同步:获取与释放的完整流程
独占式同步是指同一时间只有一个线程能获取同步状态(如ReentrantLock)。AQS 通过acquire(int arg)(获取)和release(int arg)(释放)方法实现独占式操作。
3.1 独占式获取(acquire)流程
acquire(int arg)的逻辑可概括为:尝试获取状态→失败则入队→等待唤醒后重试,具体步骤如下:
- 尝试获取同步状态:调用tryAcquire(arg),若返回true,则获取成功,直接返回;
- 获取失败,封装为节点入队:
-
- 将当前线程封装为Node.EXCLUSIVE(独占模式)节点;
-
- 通过addWaiter(Node mode)方法将节点加入队列尾部(CAS 操作保证原子性);
3.阻塞等待:调用acquireQueued(Node node, int arg),节点进入自旋状态:
-
- 若当前节点的前驱是头节点,再次尝试tryAcquire(arg);
-
- 若获取成功,将当前节点设为新头节点,返回;
-
- 若前驱节点状态为SIGNAL,则通过LockSupport.park(this)阻塞当前线程;
4.唤醒后处理:线程被唤醒后,重复步骤 3,直至获取状态成功或被中断。
核心代码片段:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt(); // 若被中断,记录中断状态
}
}
3.1.1 入队操作(addWaiter)
addWaiter方法通过 CAS 操作将节点加入队列尾部,若尾节点为null(队列未初始化),则先初始化队列:
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)) { // CAS设置新尾节点
pred.next = node;
return node;
}
}
enq(node); // 队列未初始化或CAS失败,通过enq初始化并入队
return node;
}
private Node enq(final Node node) {
for (;;) { // 自旋确保入队成功
Node t = tail;
if (t == null) { // 初始化队列,创建头节点
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3.2 独占式释放(release)流程
release(int arg)的逻辑是:尝试释放状态→成功则唤醒后继节点,具体步骤如下:
- 尝试释放同步状态:调用tryRelease(arg),若返回true,则释放成功;
- 唤醒后继节点:
-
- 获取头节点,若头节点不为null且状态不为 0,调用unparkSuccessor(Node node)唤醒后继节点;
-
- 后继节点被唤醒后,会重新尝试获取同步状态。
核心代码片段:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h); // 唤醒后继节点
}
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0); // 清除状态
}
Node s = node.next;
if (s == null || s.waitStatus > 0) { // 后继节点已取消
s = null;
// 从尾节点向前查找第一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
if (s != null) {
LockSupport.unpark(s.thread); // 唤醒线程
}
}
四、共享式同步:多线程共享资源的实现
共享式同步允许多个线程同时获取同步状态(如CountDownLatch、Semaphore)。AQS 通过acquireShared(int arg)(获取)和releaseShared(int arg)(释放)方法实现共享式操作。
4.1 共享式获取(acquireShared)流程
acquireShared(int arg)的逻辑与独占式类似,但允许多个线程同时成功:
- 尝试获取共享状态:调用tryAcquireShared(arg),返回值≥0 表示成功,<0 表示失败;
- 失败则入队:通过doAcquireShared(arg)将线程封装为Node.SHARED节点入队;
- 等待唤醒:节点进入自旋,若前驱是头节点,再次尝试获取;若成功,唤醒后续共享节点(通过setHeadAndPropagate传播状态);若失败,则阻塞等待。
核心代码片段:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) { // 获取成功
setHeadAndPropagate(node, r); // 设为头节点并传播状态
p.next = null; // 帮助GC
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
// 阻塞当前线程,直至被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
状态传播(setHeadAndPropagate):共享模式下,当前节点获取状态成功后,需唤醒后续共享节点(如CountDownLatch计数器归 0 后,所有等待线程都应被唤醒)。
4.2 共享式释放(releaseShared)流程
releaseShared(int arg)的逻辑是:尝试释放状态→成功则唤醒后继节点,且可能唤醒多个节点(与独占式不同):
- 尝试释放共享状态:调用tryReleaseShared(arg),返回true表示释放成功;
- 唤醒后继节点:通过doReleaseShared()唤醒队列中的节点,且会传播唤醒(即唤醒一个节点后,该节点可能继续唤醒下一个)。
核心代码片段:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 后继节点需要唤醒
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue; // CAS失败,重试
}
unparkSuccessor(h); // 唤醒后继
} else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue; // 设为PROPAGATE,确保状态传播
}
}
if (h == head) { // 头节点未变化,退出循环
break;
}
}
}
五、条件队列:线程间协作的补充机制
AQS 通过条件队列(Condition) 支持线程间的协作,类似于synchronized中的wait()/notify(),但更灵活(支持多个条件队列)。Condition的实现依赖于 AQS 的节点结构,与同步队列形成互补。
5.1 条件队列的结构
每个Condition对象对应一个单向链表的条件队列,节点类型为Node.CONDITION。当线程调用Condition.await()时,会从同步队列转移到条件队列并阻塞;当调用Condition.signal()时,会将条件队列的节点转移到同步队列,等待获取同步状态。
核心操作:
- await():释放同步状态,将线程加入条件队列并阻塞;
- signal():将条件队列的头节点转移到同步队列,等待唤醒;
- signalAll():将条件队列的所有节点转移到同步队列。
5.2 与同步队列的交互
条件队列与同步队列的交互是线程协作的关键:
- 线程获取同步状态后,调用await():释放状态→加入条件队列→阻塞;
- 其他线程调用signal():将条件队列节点转移到同步队列→唤醒线程;
- 被唤醒的线程在同步队列中重新尝试获取状态,成功后继续执行。
例如,ReentrantLock的newCondition()方法会创建一个ConditionObject(AQS 的内部类),实现条件等待功能。
代码示例:Condition 的 await 与 signal
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean flag = false;
public void waitForFlag() throws InterruptedException {
lock.lock();
try {
while (!flag) {
condition.await(); // 加入条件队列并阻塞
}
System.out.println("Flag is true, continue working");
} finally {
lock.unlock();
}
}
public void setFlag() {
lock.lock();
try {
flag = true;
condition.signal(); // 将条件队列节点转移到同步队列
} finally {
lock.unlock();
}
}
}
在上述示例中,调用waitForFlag()的线程会因flag为false进入条件队列;当setFlag()被调用后,flag置为true,条件队列的线程被转移到同步队列,等待获取锁后继续执行。
六、AQS 的应用:并发工具的底层依赖
AQS 是 Java 并发工具的 “基因”,理解 AQS 能帮助我们更深入地掌握这些工具的工作原理。
6.1 ReentrantLock 与 AQS
ReentrantLock通过重写 AQS 的独占式方法实现:
- tryAcquire(int arg):通过 CAS 尝试获取锁,支持重入(state+1)和公平性判断;
-
- 公平锁:获取前先检查队列中是否有前驱节点,无则尝试 CAS;
-
- 非公平锁:直接尝试 CAS,失败后再入队;
- tryRelease(int arg):释放锁(state-1),当state为 0 时表示完全释放,返回true。
公平锁与非公平锁的性能差异:非公平锁因省去队列检查步骤,在高并发场景下吞吐量更高,但可能导致线程饥饿;公平锁虽保证顺序,但因频繁的队列操作会增加开销。
6.2 CountDownLatch 与 AQS
CountDownLatch通过重写 AQS 的共享式方法实现:
- 初始化时,state设为计数器值(如new CountDownLatch(3)则state=3);
- tryAcquireShared(int arg):判断state是否为 0,是则返回 0(成功),否则返回 - 1(失败);
- tryReleaseShared(int arg):通过 CAS 将state减 1,当state变为 0 时返回true,触发所有等待线程唤醒。
典型场景:主线程等待多个子线程完成初始化,子线程全部执行countDown()后,主线程从await()返回。
6.3 Semaphore 与 AQS
Semaphore(信号量)用于控制同时访问资源的线程数量,其 AQS 实现逻辑为:
- state表示可用许可数量;
- 非公平模式tryAcquireShared(int arg):直接 CAS 减少state,成功则返回剩余许可;
- 公平模式tryAcquireShared(int arg):先检查队列,无等待线程再 CAS 减少state;
- tryReleaseShared(int arg):CAS 增加state,释放许可。
示例:Semaphore(5)允许 5 个线程同时获取许可,第 6 个线程需等待其他线程释放许可。
七、AQS 的设计智慧与局限
7.1 设计亮点
- 模板模式的极致应用:将共性逻辑(排队、唤醒)与个性逻辑(状态判断)分离,极大降低同步器实现难度;
- 高效的队列管理:基于 CLH 队列的双向链表设计,结合 CAS 操作实现无锁入队,减少线程阻塞;
- 多模式支持:同时支持独占式和共享式,满足不同同步场景需求;
- 内存可见性保证:state的 volatile 修饰与 CAS 操作,确保线程间状态的可见性与原子性。
7.2 局限性
- 单一状态变量:state为 int 类型,复杂同步器可能需要多个状态,需通过位运算拆分(如ReentrantReadWriteLock用state高 16 位表示读锁,低 16 位表示写锁);
- 线程唤醒的不确定性:唤醒操作依赖LockSupport.unpark(),但线程何时被调度由操作系统决定,可能存在延迟;
- 自定义难度高:需深入理解 AQS 机制才能正确实现同步器,否则易出现死锁、饥饿等问题。
八、总结:AQS 在并发体系中的地位
AQS 是 Java 并发编程的 “基础设施”,它以简洁而强大的设计支撑了众多并发工具的实现。其核心价值在于:
- 抽象共性:将同步器的通用逻辑(排队、唤醒、状态管理)抽象为模板方法,避免重复开发;
- 灵活扩展:通过状态变量和钩子方法,允许子类根据需求定制同步规则;
- 性能优化:基于 CAS 和自旋的无锁操作,减少线程上下文切换开销。
理解 AQS 不仅是掌握ReentrantLock、CountDownLatch等工具的关键,更是深入领会 Java 并发编程思想的必经之路。它的设计理念 ——用状态控制访问权限,用队列管理等待线程—— 为解决复杂并发问题提供了通用思路,也为自定义同步器开发奠定了坚实基础。