1. 引言
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发包(java.util.concurrent)中最核心的基础组件之一。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等同步工具,其底层实现都依赖于 AQS。
理解 AQS 是深入掌握 Java 并发编程的关键一步。本文将从 AQS 的设计思想出发,逐步剖析其核心数据结构、同步状态管理、CLH 队列的入队与出队流程,并结合 ReentrantLock 的源码,带你彻底搞懂 AQS 的工作原理。
2. AQS 是什么
AQS 是一个用于构建锁和同步器的框架。它提供了一个基于 FIFO(先进先出)等待队列的阻塞锁机制,以及一个表示同步状态的 volatile int state 字段。
使用 AQS 可以方便地实现自定义同步器,只需重写以下几个方法即可:
tryAcquire(int arg):独占式获取同步状态tryRelease(int arg):独占式释放同步状态tryAcquireShared(int arg):共享式获取同步状态tryReleaseShared(int arg):共享式释放同步状态isHeldExclusively():当前线程是否独占资源
AQS 的设计采用了模板方法模式,它定义了获取和释放同步状态的骨架,而将具体的获取/释放逻辑留给子类实现。
3. 核心数据结构
3.1 同步状态 state
AQS 内部维护了一个 volatile int state 字段,用于表示同步状态:
java
private volatile int state;
- 对于
ReentrantLock,state表示锁被重入的次数; - 对于
Semaphore,state表示剩余的许可数量; - 对于
CountDownLatch,state表示还需要等待的计数。
state 的读写提供了三种方法:
java
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);
}
其中 compareAndSetState 基于 CAS(Compare-And-Swap)操作,保证并发环境下状态更新的原子性。
3.2 CLH 队列节点 Node
AQS 内部使用一个 CLH 队列 (Craig, Landin, and Hagersten 锁队列的变体)来管理等待线程。队列中的每个节点都是一个 Node 对象:
java
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 等待状态:取消
static final int CANCELLED = 1;
// 等待状态:后继节点需要唤醒
static final int SIGNAL = -1;
// 等待状态:条件等待
static final int CONDITION = -2;
// 等待状态:共享式传播
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
waitStatus 是节点的等待状态,初始值为 0。当节点被取消时置为 CANCELLED;当节点的后继线程需要被唤醒时置为 SIGNAL。
4. 获取锁的流程
以独占模式为例,获取同步状态的入口方法是 acquire(int arg):
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
整个流程分为三步:
- 尝试获取 :调用
tryAcquire(arg)尝试获取同步状态,成功则直接返回; - 入队 :获取失败,调用
addWaiter(Node.EXCLUSIVE)将当前线程封装成节点加入等待队列尾部; - 自旋等待 :调用
acquireQueued()让节点在队列中自旋,直到获取到同步状态或被中断。
4.1 入队操作 addWaiter
java
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);
return node;
}
入队时先尝试快速入队:将新节点的 prev 指向当前尾节点,然后通过 CAS 将尾节点更新为新节点。如果 CAS 失败(说明有竞争),则进入 enq() 方法通过自旋 + CAS 的方式保证入队成功。
4.2 自旋等待 acquireQueued
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
节点在队列中自旋,只有当它的前驱节点是头节点时,才会再次尝试获取锁。如果前驱不是头节点,或者获取失败,则调用 shouldParkAfterFailedAcquire 判断是否需要挂起当前线程。
5. 释放锁的流程
独占模式释放同步状态的入口方法是 release(int arg):
java
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放流程相对简单:
- 调用
tryRelease(arg)尝试释放同步状态; - 释放成功后,找到头节点,调用
unparkSuccessor(h)唤醒头节点的后继节点。
5.1 唤醒后继节点 unparkSuccessor
java
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);
}
唤醒时,如果后继节点为 null 或已被取消(waitStatus > 0),则从尾节点向前遍历,找到最靠近头节点的有效节点进行唤醒。
6. ReentrantLock 的实现
ReentrantLock 是 AQS 最典型的应用。它内部定义了一个继承 AQS 的同步器 Sync,并分为公平锁 FairSync 和非公平锁 NonfairSync 两种实现。
6.1 非公平锁
java
static final class NonfairSync extends Sync {
final void lock() {
// 先直接尝试 CAS 抢占一次
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
非公平锁在 lock() 时,会先尝试直接 CAS 抢占锁,如果成功则立即获得锁,不进入队列。这就是「非公平」的体现------新来的线程可能插队。
6.2 公平锁
java
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁:先检查队列中是否有前驱节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁在获取锁之前,会先调用 hasQueuedPredecessors() 检查等待队列中是否有前驱节点。如果有,则当前线程必须排队,不能插队。
6.3 可重入的实现
ReentrantLock 的可重入性体现在 tryAcquire 中:当 state > 0 且当前线程是持有锁的线程时,state 累加,表示重入次数增加。释放时 state 递减,直到 state == 0 才真正释放锁。
7. 总结
AQS 是 Java 并发编程的基石,其核心设计可以概括为以下几点:
- 一个状态 :
volatile int state表示同步状态; - 一个队列:CLH 变体队列管理等待线程;
- 两种模式:独占模式与共享模式;
- 三种操作:获取、释放、条件等待。
理解 AQS 的关键在于把握「模板方法模式 + CAS + 自旋 + 阻塞唤醒」的组合。掌握了 AQS,再去看 ReentrantLock、Semaphore、CountDownLatch 等同步器的源码,就会豁然开朗。