Java 并发基石:AQS (AbstractQueuedSynchronizer)
1. 引言
在 Java java.util.concurrent (JUC) 包中,锁(Lock)、信号量(Semaphore)、倒计数器(CountDownLatch)等高频并发工具类的背后,都有一个共同的基石------AbstractQueuedSynchronizer(简称 AQS)。
AQS 是由 Doug Lea 大神设计的一个用来构建锁和同步器的框架。它屏蔽了复杂的线程阻塞、唤醒机制以及等待队列的管理,通过模板方法模式,让上层应用只需实现少量的特定方法即可构建功能强大的同步组件。
2. AQS 核心架构概览
AQS 的核心思想是将并发控制逻辑抽象为对一个共享资源状态(state)的维护,以及对竞争资源失败的线程进行排队管理。
2.1 核心组件
AQS 类定义中最重要的几个字段如下:
java
private volatile int state; // 同步状态
private transient volatile Node head; // 队列头(哨兵或持有锁的节点)
private transient volatile Node tail; // 队列尾
private transient Thread exclusiveOwnerThread; // (继承自 AOS) 独占模式下的持有线程
1. 同步状态 (state)
-
本质 :
state是所有并发逻辑的判断依据,必须是volatile的,配合Unsafe.compareAndSwapInt (CAS)实现原子性修改。 -
工作机制 :AQS 框架本身并不关心
state具体等于几,它只提供 CAS 方法。对于
ReentrantLock,state 表示锁的重入次数(0表示无锁)。对于
Semaphore,state 表示剩余许可数量。对于
CountDownLatch,state 表示剩余的计数。- 抢锁:本质就是 CAS(state, 0, 1)(以 ReentrantLock 为例)。CAS 成功,CPU 缓存行同步,线程获得执行权;CAS 失败,说明有竞争,线程准备入队。
- 内存可见性 :根据 JMM(Java 内存模型)的 happens-before 规则,对
volatile变量的写操作先行发生于后续的读操作。这意味着:线程释放锁修改state之前的所有操作,对后续拿到锁读取state的线程都是可见的。这是 AQS 保证线程安全的基础。
2. CLH 变体队列
- 设计痛点:如果成千上万个线程争抢一个锁,直接 CAS 会导致总线风暴(Bus Storm)。AQS 需要一个队列让失败的线程"排队睡觉"。
- 结构特性 :双向链表(Node)。
- Head(头节点) :严格来说,Head 指向的节点代表当前持有锁的线程(或者是刚刚释放锁、正在唤醒后继的节点)。它是一个"哨兵",不存储具体的等待线程信息。
- Tail(尾节点):新来的请求失败线程,通过 CAS 操作拼接到 Tail 后面。
- 核心逻辑 :
- 前驱负责唤醒后继 :AQS 的队列运行机制是高度被动 的------当前节点无法自我唤醒,全靠前驱节点(Predecessor)在释放资源时传递信号。因此,维护"唤醒链条"的连通性 至关重要。当一个线程获取锁失败并入队后,在它真正调用
LockSupport.park()挂起自己之前,必须执行一个关键的**"前驱探查"**动作(源码中的shouldParkAfterFailedAcquire方法):- 剔除死链(Skip Cancelled) :当前线程显然是活跃的(正在运行代码),但它的前驱节点可能因为超时或中断而处于
CANCELLED(放弃)状态。一个已废弃的节点永远不会执行唤醒操作,如果直接排在它后面,当前线程将面临"永久阻塞"的风险。
因此,当前节点会检查前驱的waitStatus,如果是CANCELLED,则修改prev指针跳过 该节点,并循环向前查找,直到找到一个有效且正常 的节点挂在它后面。这种由后继者负责清理前驱的机制,实现了对队列中废弃节点的惰性移除 (Lazy Removal),确保了链表的活性。 - 建立信号契约(Signal Setup) :找到有效前驱后,当前节点需要利用 CAS 将前驱的状态修改为
SIGNAL(-1)。。只有完成了这个信号契约,当前线程才会安心地调用 park() 进入阻塞状态。
- 剔除死链(Skip Cancelled) :当前线程显然是活跃的(正在运行代码),但它的前驱节点可能因为超时或中断而处于
- 只有当前驱节点释放锁时,才会定向唤醒(unpark)它的直接后继节点。这种点对点的唤醒避免了所有等待线程同时醒来抢锁。
- 前驱负责唤醒后继 :AQS 的队列运行机制是高度被动 的------当前节点无法自我唤醒,全靠前驱节点(Predecessor)在释放资源时传递信号。因此,维护"唤醒链条"的连通性 至关重要。当一个线程获取锁失败并入队后,在它真正调用
3. 持有者线程 (exclusiveOwnerThread)
- 作用:仅用于独占模式。
- 逻辑闭环 :当线程尝试获取锁时,会判断
current_thread == exclusiveOwnerThread。- 如果相等,说明是重入 ,不需要 CAS 抢锁,直接
state++即可。 - 这是
ReentrantLock能够实现递归调用的底层支撑。
- 如果相等,说明是重入 ,不需要 CAS 抢锁,直接
2.2 资源共享模式
这两种模式的区别不仅仅是"一个线程"和"多个线程",真正的区别在于唤醒后的行为(Propagation)。
1. Exclusive (独占模式)
- 代表 :
ReentrantLock - 行为 :单点唤醒 。
- 持有锁的线程释放锁 (release) -> 唤醒 Head 的后继节点 (Next) -> Next 醒来抢到锁 -> Next 执行。
- 整个过程是一对一的接力。
2. Share (共享模式)
- 代表 :
CountDownLatch,Semaphore - 行为 :级联传播 (Propagation) 。
- 这就是共享模式最难理解的地方。当一个节点在共享模式下被唤醒并成功获取资源(比如 state 从 0 变为 1,或者 Semaphore 还有剩余许可)后,它不仅自己醒来执行,还会判断是否需要唤醒它的后继节点。
- 源码逻辑 :
setHeadAndPropagate()。如果资源还有剩余(tryAcquireShared返回值 > 0),当前节点会继续触发doReleaseShared(),唤醒下一个节点。这就形成了一个多米诺骨牌效应 ,使得多个等待线程可以在瞬间几乎同时被唤醒(例如CountDownLatch倒计时归零时)。
3. 内部数据结构:Node 节点
CLH 队列中的节点由内部类 Node 定义。理解 Node 的字段及其状态是理解 AQS 流程的关键。
java
static final class Node {
// 模式定义
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// 线程等待状态 (waitStatus) 常量
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;
// 指向下一个处于 Condition 等待的节点(用于 ConditionObject)
Node nextWaiter;
// ...
}
3.1 waitStatus 详解
waitStatus 初始化为 0,表示普通同步节点。其他状态含义如下:
- CANCELLED (1):当前节点内的线程因为超时或中断被取消。该状态是终态,一旦设置不会改变。
- SIGNAL (-1) :非常关键的状态 。表示当前节点的后继节点处于阻塞(park)状态。当前节点在释放或取消时,必须唤醒(unpark)它的后继节点。
- CONDITION (-2):节点处于条件队列(Condition Queue)中,等待被 signal。
- PROPAGATE (-3):仅用于共享模式,表示下一次共享式同步状态获取将会无条件传播下去。
3.2 队列结构图示
AQS 队列包含了 head 和 tail 两个指针。
- 初始化时:head 和 tail 指向同一个空的 Dummy Node(哨兵节点)。
- 入队:新节点利用 CAS 插到 tail 后面。

4. 独占模式源码解析 (Acquire & Release)
我们以独占模式(如 ReentrantLock.lock())为例,完整剖析线程从尝试获取锁到阻塞,再到被唤醒的完整闭环。
4.1 获取资源:acquire(int arg)
这是 AQS 的顶层入口方法,采用模板方法模式:
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
逻辑:
tryAcquire(arg):尝试直接获取资源。这是一个抽象方法,由子类实现(如ReentrantLock里的非公平锁实现直接 CASstate)。如果成功,直接返回。addWaiter(Node.EXCLUSIVE):如果获取失败,将当前线程封装成 Node,加入等待队列尾部。acquireQueued(node, arg):节点入队后,进入自旋+阻塞逻辑。selfInterrupt():如果在等待过程中被中断过,获取锁后补一个中断信号。
4.1.1 入队逻辑:addWaiter
如果 tryAcquire 失败,线程需要进入等待队列。AQS 采用 CLH 变体队列 (双向链表),新节点总是插入到 tail 后面。
java
private 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;
}
enq(node) 方法通过死循环(自旋)保证节点一定能插入队尾。
注意 :首次入队时,队列是空的(head 和 tail 都是 null)。enq 方法会先创建一个空的 Dummy Node (哨兵节点)作为 head,然后再把当前线程的节点接在后面。因此,队列中永远至少有一个节点。
4.1.2 挂起核心:acquireQueued
这是 AQS 最复杂也是最核心的部分。
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱
final Node p = node.predecessor();
// 只有前驱是 head 时,才有资格尝试获取锁
// 因为 head 是持有锁的节点(或者刚释放完),轮也轮到 head 的下一个了
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,自己成为 head (dummy)
p.next = null; // help GC
failed = false;
return interrupted;
}
// 获取失败,判断是否需要挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
关键点解析:
- 自旋检查 :并不是一入队就立刻阻塞。如果前驱是 Head,会再次尝试
tryAcquire。这是为了提升性能,也许前一个线程刚刚释放锁。 shouldParkAfterFailedAcquire(p, node):- 该方法主要作用是修改前驱节点
p的waitStatus。 - 如果
p.waitStatus是SIGNAL,说明前驱已经知道要唤醒我了,返回true。 - 如果
p.waitStatus> 0(CANCELLED),说明前驱放弃了,向前查找直到找到一个正常的节点,挂在它后面。 - 如果是 0 或 PROPAGATE,利用 CAS 将前驱状态置为
SIGNAL,返回false(不阻塞,外层循环再试一次,确保 CAS 成功)。
- 该方法主要作用是修改前驱节点
parkAndCheckInterrupt():- 调用
LockSupport.park(this)挂起当前线程。线程停在这里,不再消耗 CPU。 - 当被
unpark或中断时,从这里醒来,返回中断状态。
- 调用
4.2 释放资源:release(int arg)
释放锁的逻辑相对简单,核心在于修改 State 并唤醒 Head 的后继。
java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现,修改 state
Node h = head;
// 如果头节点存在且状态不为0(说明有后继需要唤醒)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
4.2.1 唤醒后继:unparkSuccessor
java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 将 head 的状态置零,清除 SIGNAL 标记
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果 next 节点为空 或者 next 被取消了
if (s == null || s.waitStatus > 0) {
s = null;
// 从 tail 向前遍历,找到离 head 最近的一个有效节点
// 为什么要从后往前?因为 addWaiter 中是先设置 prev 再 CAS tail,
// 最后才设置 pred.next。next 指针在并发下可能不一致或为空。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒找到的有效节点
if (s != null)
LockSupport.unpark(s.thread);
}
注意:为什么唤醒时要从 Tail 向前遍历查找?
这与 addWaiter 中的入队逻辑有关。在节点入队时:
java
node.prev = pred; // 1. 先设置 prev
if (compareAndSetTail(pred, node)) { // 2. CAS 设置 tail
pred.next = node; // 3. 最后设置 next
}
这是一个非原子 的操作。在步骤 2 执行成功(tail 已更新)但步骤 3 还没执行(next 指针还没连上)的瞬间,如果此时执行唤醒逻辑,通过 head.next 是找不到新节点的。
但此时 node.prev 已经连接成功。因此,从 tail 通过 prev 指针向前遍历是绝对安全的,能保证不会漏掉刚刚入队的节点。
4.0 AQS 独占模式全流程图
图 1:入队阶段 (Acquire & AddWaiter)
描述线程尝试获取锁失败后,初始化队列(如果需要)并将自己封装为 Node 加入队尾的过程。

图 2:核心循环阶段 (AcquireQueued - 自旋与挂起)
这是 AQS 的心脏。描述节点入队后,如何在"尝试抢锁"和"阻塞睡觉"之间切换,以及如何处理"放弃"的前驱节点。
图 3:释放与唤醒阶段 (Release & Unpark)
描述持有锁的线程释放资源,并查找下一个合适的节点进行唤醒的过程。重点在于从后往前找的兜底逻辑。
5. 共享模式 (Shared) 的区别
共享模式(如 CountDownLatch)与独占模式的主要区别在于:当一个线程获取同步状态成功后,会继续传播唤醒等待在后面的共享节点。
5.1 doAcquireShared
流程大体一致,但在获取资源成功后,调用 setHeadAndPropagate(node, r)。
java
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// 如果 propagate > 0 (剩余资源 > 0) 或者旧/新 head 状态为 PROPAGATE/SIGNAL
if (propagate > 0 || h == null || h.waitStatus < 0 || ... ) {
Node s = node.next;
// 如果后继是共享模式,继续唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
这就是为什么 CountDownLatch.countDown() 减到 0 时,所有 await() 的线程会几乎同时被唤醒的原因------级联唤醒。
6. ConditionObject 机制
AQS 的强大之处在于它不仅仅支持简单的"抢锁",还通过内部类 ConditionObject 实现了类似 Object.wait/notify 的高级同步语义。
要理解 Condition,必须首先建立 "双队列模型" 的认知。
6.1 同步队列 vs 条件队列
AQS 内部实际上存在两套队列系统,它们共用 Node 类,但组织方式和用途完全不同:
-
同步队列 (Sync Queue / CLH):
- 用途:存储争抢锁失败的线程。
- 结构 :双向链表 (
prev+next)。 - 状态 :关注
SIGNAL(-1) 和CANCELLED(1)。 - 并发:入队出队涉及激烈的 CAS 竞争。
-
条件队列 (Condition Queue):
- 用途 :存储调用了
await()等待某些条件的线程。 - 结构 :单向链表 (
nextWaiter)。 - 状态 :关注
CONDITION(-2)。 - 并发 :无锁竞争 。因为调用
await()或signal()时,线程必须已经持有锁。
- 用途 :存储调用了
6.1.1 结构示意图

6.2 await()
当线程调用 condition.await() 时,执行:主动放弃锁 -> 进入条件队列 -> 等待被召回 -> 重新抢锁。
6.2.1 核心源码剖析
java
public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
// 1. 【入队】:将当前线程封装为 Node,加入条件队列尾部
Node node = addConditionWaiter();
// 2. 【彻底释放锁】:为什么要 releaseAll?
// 因为 ReentrantLock 是可重入的,state 可能为 5。
// await 时必须一次性把 state 减为 0,让出锁给别人,并保存 savedState 用于将来恢复。
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 【挂起】:只要节点还在 Condition 队列中(没被 signal 移到 Sync 队列),就一直阻塞
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 处理中断唤醒的情况...
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 【复活抢锁】:跳出 while 说明被 signal 了(或者被中断),
// 此时节点已经回到了 Sync 队列。
// 调用 acquireQueued 像普通线程一样排队抢锁,目标是将 state 恢复为 savedState。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// ... 清理逻辑
}
6.2.2 关键步骤详解
addConditionWaiter():- 不需要 CAS!因为当前线程持有锁。直接操作
lastWaiter指针将新节点挂到单向链表末尾,并将waitStatus设为CONDITION(-2)。
- 不需要 CAS!因为当前线程持有锁。直接操作
fullyRelease(node):- 这是一个原子性的操作。如果失败(比如没拿锁就调 await),会抛出
IllegalMonitorStateException并将节点状态置为 CANCELLED。
- 这是一个原子性的操作。如果失败(比如没拿锁就调 await),会抛出
isOnSyncQueue(node):- 这是判断"我是谁?我在哪?"的关键。
signal()的本质是将节点从条件队列迁移到同步队列。 - 如果
isOnSyncQueue返回 false,说明还没有其它线程signal,那继续park。
- 这是判断"我是谁?我在哪?"的关键。
6.3 signal()
signal() 并不直接唤醒线程让它运行,而是将它从条件队列(Condition Queue)移到同步队列(Sync Queue)。
6.3.1 核心源码剖析
java
public final void signal() {
if (!isHeldExclusively()) // 必须持有锁
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 修改指针,将 first 从条件队列移除
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // 核心转移逻辑
(first = firstWaiter) != null);
}
6.3.2 核心转移逻辑:transferForSignal
java
final boolean transferForSignal(Node node) {
// 1. CAS 将节点状态从 CONDITION (-2) 改为 0
// 如果失败,说明节点在 Condition 队列里已经取消了,返回 false 继续找下一个
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 2. 【入队】:调用 enq(node) 将节点放入 Sync 队列尾部
// p 是 node 在 Sync 队列中的前驱
Node p = enq(node);
int ws = p.waitStatus;
// 3. 【可能唤醒】:
// 这里的逻辑非常微妙!通常 signal 只是把节点移动到 Sync 队列,不唤醒。
// 只有当:
// a. 前驱节点 p 已经取消了 (ws > 0)
// b. 或者无法设置前驱 p 的状态为 SIGNAL (无法让它负责唤醒我)
// 这时候才迫不得已,直接 unpark 唤醒当前线程,让它自己去修正前驱状态。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
重点误区 :
绝大多数情况下,signal() 只是把节点从 Condition Queue 搬运到了 Sync Queue。线程依然在 await() 的 while 循环中阻塞着(或者刚被移过去,还没轮到它)。真正的唤醒通常发生在持有锁的线程调用 unlock() 时,通过 AQS 的正常释放流程唤醒 Sync Queue 中的 Head.next。
6.4 Condition 完整交互流程图
这张图展示了线程 A(等待者)和线程 B(唤醒者)如何在两个队列间配合。

6.5 总结 Condition 的设计哲学
- 条件谓词的原子性 :
await必须先释放锁再挂起,signal必须持有锁,这保证了检查条件和操作等待队列是原子的,不会出现"丢失信号"的问题。 - 队列隔离 :将"等待条件"和"等待锁"分离。
signal的本质是将线程从"关注条件"的状态转换为"关注锁"的状态。 - 复用机制 :Condition 队列的节点被移到 Sync 队列后,完全复用了 AQS 原有的排队、阻塞、唤醒机制(
acquireQueued)。
7. AQS 实战应用解析
AQS 的强大之处在于它将"锁"的概念泛化了。子类通过定义 state 的含义,并重写 tryAcquire / tryRelease 系列方法,即可实现形态迥异的同步器。
7.1 ReentrantLock:可重入的独占锁
ReentrantLock 的核心诉求是:排他性 与可重入性。
1. State 语义映射
- State = 0 :自由态。表示当前锁未被任何线程持有。
- State > 0 :锁定态 。数值表示当前持有线程的重入次数(Reentrancy Count)。
- ExclusiveOwnerThread:必须记录是谁拿了锁,用于判断是否是重入。
2. tryAcquire (非公平锁实现)
非公平锁的特点是"抢占"。新来的线程不需要管队列里有没有人排队,直接尝试 CAS 改写 state。
java
// ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 1. 自由态:直接 CAS 抢占
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 标记"我"是主人
return true; // 告诉 AQS:获取成功,不用入队
}
}
// 2. 锁定态:检查是不是自己人(可重入逻辑)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 计数累加
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc); // 不需要 CAS,因为只有持有者能走到这,单线程操作
return true;
}
// 3. 竞争失败
return false; // 告诉 AQS:获取失败,把我放入队列阻塞吧
}
3. tryRelease (递归释放)
释放锁不仅仅是将 state 设为 0,而是做减法。只有减到 0 时,才算真正释放了物理锁。
java
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 状态递减
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 防御性编程:没拿锁的人不能释放
boolean free = false;
// 只有减到 0,才意味着锁被彻底释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 清空持有者
}
setState(c); // 更新 state
return free; // 返回 true 才会触发 AQS 唤醒后继节点 (unparkSuccessor)
}
7.2 CountDownLatch:倒计时
CountDownLatch 是典型的共享模式 (Shared) 应用。它的逻辑是:count > 0 之前,所有人都要等;一旦 count == 0 ,所有人同时通过。
1. State 语义映射
- State > 0 :闭锁关闭 。代表还需要等待
countDown的次数。 - State = 0 :闭锁开启。所有等待线程被释放,且新来的线程不会被阻塞。
2. await() -> tryAcquireShared
await() 方法内部调用的是 AQS 的 acquireSharedInterruptibly(1)。
这里的核心在于:它并不是要真的"获取"什么资源,而是判断"能不能通过"。
java
protected int tryAcquireShared(int acquires) {
// 这里的返回值含义:
// < 0 : 获取失败,进入队列阻塞
// >= 0 : 获取成功,通行
// 逻辑极其简单:
// 如果 state == 0,返回 1 (成功,且允许传播唤醒)
// 如果 state > 0,返回 -1 (失败,进入队列阻塞)
return (getState() == 0) ? 1 : -1;
}
3. countDown() -> tryReleaseShared
每次调用 countDown,state 减 1。关键点在于减到 0 的那一刻。
java
protected boolean tryReleaseShared(int releases) {
// 必须用死循环 CAS,因为可能有多个线程同时 countDown
for (;;) {
int c = getState();
if (c == 0) // 已经归零了,无需再减
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc)) {
// 只有从 1 变成 0 的那个线程,返回 true
// 这会触发 AQS 调用 doReleaseShared(),从而唤醒所有等待线程
return nextc == 0;
}
}
}
7.3 Semaphore:流量控制的信号量
Semaphore 也是共享模式,但它与 CountDownLatch 不同。Latch 是一次性的,而 Semaphore 可以实现动态的增减。
1. State 语义映射
- State :剩余许可证(Permits)数量。
- 逻辑:线程获取许可,State 减 N;线程释放许可,State 加 N。
2. acquire() -> tryAcquireShared
这里展示了共享模式下如何处理"资源不足"和"并发竞争"。
java
// 非公平模式实现
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 1. 如果 remaining < 0:
// 说明资源不够,直接返回负数。AQS 会把线程放入队列。
// 2. 如果 remaining >= 0:
// 说明资源足够,尝试 CAS 修改 state。
// 如果 CAS 成功,返回剩余量(>=0),代表获取成功。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
注意 :tryAcquireShared 的返回值不仅表示成功/失败,还表示**"剩余资源量"。如果返回值 > 0,AQS 的 setHeadAndPropagate 逻辑会尝试唤醒后继节点**,这对于提高 Semaphore 的吞吐量至关重要(因为可能还剩很多资源,应该让排队的后面几个人都醒过来)。
3. release() -> tryReleaseShared
释放就是简单的"归还资源",通过 CAS 增加 state。
java
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // 溢出检查
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true; // 只要释放成功,就返回 true,AQS 会去唤醒等待线程
}
}
7.4 子类实现的三个关键点
通过对比可以发现,实现自定义同步器主要关注三点:
- 定义 State:这个整数到底代表什么?(锁重入次数?倒计时?剩余许可证?)
- 定义成功与失败 :
- 独占模式下,
tryAcquire返回boolean。 - 共享模式下,
tryAcquireShared返回int(负数=失败,0=成功但不传播,正数=成功且传播)。
- 独占模式下,
- 定义原子操作 :使用
compareAndSetState循环处理状态变更,解决并发冲突。
**重点:**这套机制完美体现了 AQS "Template Method Pattern" (模板方法模式) 的精髓:
-
AQS 负责复杂的排队和线程调度。比如:线程的排队(addWaiter)、阻塞(park)、唤醒(unparkSuccessor)以及队列维护。这些方法是 final 的,子类无法修改。
-
子类负责轻量的业务语义映射。只需要实现
tryAcquire、tryRelease等特定方法,告诉 AQS "如何判断抢锁成功"以及"如何修改 State"即可。
8. 总结
AbstractQueuedSynchronizer 摒弃了传统的 synchronized 重量级锁依赖,通过 State (资源状态) + CLH Queue (线程等待队列) + LockSupport (阻塞机制) 构建了一套强大的同步框架。
- 原子性 :依赖 CAS 操作
state和队列指针。 - 阻塞 :依赖
LockSupport.park/unpark,避免了Object.wait对 monitor 锁的依赖。 - 队列 :采用 CLH 变体,通过前驱节点的
waitStatus来决定当前节点是阻塞还是唤醒,精妙地解决了并发下的自旋与上下文切换开销问题。

