AQS(AbstractQueuedSynchronizer)是 java.util.concurrent(JUC)包的基石。我们熟知的 ReentrantLock、Semaphore、CountDownLatch 等同步工具,其底层核心逻辑都构建在 AQS 之上。
可以毫不夸张地说,理解了 AQS,就掌握了 Java 并发工具包大半的奥秘。
一、什么是 AQS?
AQS 是一个抽象类 ,无法被实例化。它的设计采用了模板方法模式 ,将获取/释放锁的复杂逻辑(如同步队列的维护、线程的阻塞与唤醒)封装在内部,而将与"具体同步状态"判断相关的逻辑(如 tryAcquire、tryRelease)抽象出来,留给子类去实现。
核心思想 :如果共享资源空闲,则请求线程直接获取锁;否则,将请求线程封装成一个
Node节点,加入一个 FIFO 的同步队列中排队等待。
二、AQS 的核心三要素
AQS 的运行机制主要围绕三个核心组件构建。
2.1 同步状态:volatile int state
state 是一个 volatile 修饰的 int 类型变量,是 AQS 的"灵魂"所在。
-
它代表共享资源的状态。在不同同步器中含义不同:
-
ReentrantLock:state为 0 表示未锁定,>0 表示锁定,其值还表示重入次数。 -
Semaphore:state表示剩余许可证数量。 -
CountDownLatch:state表示需要等待的计数。
-
-
对
state的修改必须通过 CAS 操作来保证原子性。
2.2 同步队列:CLH 队列
CLH 队列是一个虚拟的 双向链表,用于存放所有等待获取锁的线程。
-
它是一个"虚拟"的队列,因为并没有一个专门的"队列类",而是通过
Node节点中的prev和next指针关联起来的。 -
它的数据结构如下:
java
public abstract class AbstractQueuedSynchronizer {
// 同步队列的头节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;
// 同步状态
private volatile int state;
}
2.3 节点(Node):等待的线程
CLH 队列中的每个元素都是一个 Node 节点,它封装了等待的线程及其状态信息。
java
static final class Node {
// 节点等待状态,取值如下:
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 节点所代表的线程
volatile Thread thread;
// 用于表示共享/独占模式,或指向等待队列中的下一个节点
Node nextWaiter;
// 状态常量
static final int CANCELLED = 1; // 线程已取消
static final int SIGNAL = -1; // 后继线程需要被唤醒
static final int CONDITION = -2; // 线程在等待队列中(Condition)
static final int PROPAGATE = -3; // 共享模式下,唤醒操作应向后传播
}
这里最核心的是 SIGNAL 状态,它约定:当前节点的 waitStatus 为 SIGNAL ,意味着在释放锁后,有义务唤醒其后继节点。
三、独占模式(Exclusive Mode)
独占模式即同一时刻只允许一个线程持有锁 ,ReentrantLock 是其典型代表。
3.1 获取锁:acquire(int arg)
acquire 方法是独占模式下获取锁的入口。
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程拆解:
-
tryAcquire(arg)【尝试】 : 尝试直接获取锁。由子类(如ReentrantLock的Sync)实现。成功则直接返回。 -
addWaiter(Node mode)【入队】 : 尝试失败,将当前线程封装成一个独占模式的Node,并加入 CLH 队列的尾部。javaprivate Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; // 快速尝试,通过 CAS 将新节点设置为尾节点 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果快速失败,则通过循环 CAS 的方式入队 enq(node); return node; } -
acquireQueued(Node node, int arg)【等待】: 这是让线程在队列中"等待"的核心逻辑。javafinal 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; // 帮助 GC failed = false; return interrupted; } // 【阻塞】如果获取失败,判断是否应该阻塞当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }-
自旋与检查 :新节点入队后,并非立即阻塞。它会检查自己的前驱是不是
head。如果是,说明自己有机会成为下一个获取锁的线程,于是会立即再尝试一次tryAcquire。这个设计减少了不必要的线程阻塞/唤醒开销。 -
shouldParkAfterFailedAcquire【阻塞检查】 :若前驱不是头节点或尝试获取失败,此方法会检查前驱节点的waitStatus。-
如果前驱是
SIGNAL(-1),返回true,当前线程可以安全阻塞。 -
如果前驱已取消(>0),则往前跳过这些取消的节点。
-
否则,将前驱的
waitStatusCAS 设置为SIGNAL,确保将来能被唤醒。
-
-
parkAndCheckInterrupt【阻塞】 :调用LockSupport.park(this)真正将当前线程挂起。
-
3.2 释放锁:release(int arg)
锁的释放逻辑相对简单。
java
public final boolean release(int arg) {
// 1.【释放】尝试释放锁,由子类实现
if (tryRelease(arg)) {
Node h = head;
// 2.【唤醒】如果头节点不为空,且其 waitStatus 不为0(说明有义务唤醒后继)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
unparkSuccessor(Node node) 【唤醒后继】 :找到队列中第一个未被取消 的后继节点,并使用 LockSupport.unpark(thread) 唤醒它。
四、共享模式(Shared Mode)
共享模式允许多个线程同时 获取同一个锁。CountDownLatch 和 Semaphore 都是基于此模式实现的。
4.1 获取锁:acquireShared(int arg)
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 1. 尝试获取,返回值 < 0 表示失败
doAcquireShared(arg); // 2. 失败则进入队列等待
}
doAcquireShared 的逻辑与独占模式类似,区别在于:
-
节点类型标记为
SHARED。 -
获取锁成功后,会调用
setHeadAndPropagate(node, r),在成为新头节点后,如果还有剩余资源(r > 0),会继续唤醒下一个 共享节点,实现唤醒传播。
4.2 释放锁:releaseShared(int arg)
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放共享锁
doReleaseShared(); // 唤醒后继节点
return true;
}
return false;
}
doReleaseShared() 是一个自旋操作,专门用于唤醒共享模式下的后继节点,确保唤醒动作能传播下去。
五、Condition 与 AQS
Condition 接口提供了类似 Object.wait()/notify() 的功能。AQS 通过内部类 ConditionObject 来实现,它维护了一个独立的等待队列。
-
调用
condition.await(),当前线程会释放锁 ,并进入ConditionObject的等待队列。 -
调用
condition.signal(),会将等待队列中的第一个节点转移到 AQS 的同步队列中,使其有机会去竞争锁。
六、实战:自定义一个简单的互斥锁
java
// 1. 自定义一个互斥锁,实现 Lock 接口
class MyMutex implements Lock {
// 2. 内部类继承 AQS
private static class Sync extends AbstractQueuedSynchronizer {
// 3. 实现 tryAcquire(独占)
@Override
protected boolean tryAcquire(int arg) {
// 期望 state 从 0 变为 1,若成功则设置持有线程为当前线程
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 4. 实现 tryRelease(独占)
@Override
protected boolean tryRelease(int arg) {
// 只有持有锁的线程才能释放
if (getState() == 0) throw new IllegalMonitorStateException();
// 将 state 置为 0,并清空持有线程
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
private final Sync sync = new Sync();
@Override
public void lock() { sync.acquire(1); }
@Override
public void unlock() { sync.release(1); }
// ... 其他方法(tryLock, lockInterruptibly, newCondition)省略 ...
}
通过这段代码可以看到,实现一个自定义锁的核心工作就是实现 tryAcquire 和 tryRelease 这两个方法,AQS 已经为我们处理好了所有的排队和阻塞逻辑。
七、总结
AQS 的设计是 Java 并发编程智慧的集中体现:
-
资源与队列分离 :通过
state表示资源,CLH队列管理等待线程,职责清晰。 -
模板方法模式 :将复杂的同步队列管理和线程调度封装为骨架,只暴露简单的
tryAcquire等方法供子类定制。 -
灵活的双模式 :通过
tryAcquire/tryRelease和tryAcquireShared/tryReleaseShared两套 API,同时支持独占锁和共享锁。 -
高效的自旋与阻塞:在入队后先自旋尝试几次,失败后才阻塞,巧妙地平衡了性能和资源消耗。
理解了 AQS,你不仅掌握了 ReentrantLock、Semaphore 等工具的底层原理,更重要的是,你拥有了一套分析并发问题的思维框架和实现自定义同步组件的能力。