AQS及其应用
前言
在 Java 并发编程中,锁和同步器是不可或缺的工具。从 synchronized 到 ReentrantLock,从 CountDownLatch 到 Semaphore,这些工具的背后都有一个共同的基石——AbstractQueuedSynchronizer(AQS)。
AQS 是 java.util.concurrent 包的灵魂,它提供了一套框架,用于实现依赖 FIFO 等待队列的阻塞锁和同步器。JDK 中大部分的同步工具都是基于 AQS 构建的。理解 AQS 的运行机制,对于理解 Java 并发编程有着很大的帮助。
核心设计思想
AQS 的核心可以概括为两个部分:
- state:一个
volatile修饰的 int 变量,表示同步状态 - CLH 队列:一个 FIFO 的双向队列,用于存储等待获取锁的线程
state 变量
AQS 通过 volatile int state 来表示同步状态,子类通过继承 AQS 并实现其模板方法来管理 state 的变化。以 ReentrantLock 为例:
state = 0表示锁没有被持有state = 1表示锁被某个线程持有state > 1表示锁被同一个线程重入了 N 次
state 的更新使用 compareAndSetState 方法,利用 Unsafe 的 CAS 操作保证原子性。
1 | private volatile int state; |
CLH 等待队列
CLH 队列本质上是一个双向链表,AQS 将每个获取锁失败的线程封装成一个 Node 节点,并插入到队列尾部。
1 | static final class Node { |
队列的结构可以用下图来理解:
graph LR
head["head (哨兵节点)"] --> node1["Node1
thread=A
waitStatus=0"]
node1 --> node2["Node2
thread=B
waitStatus=0"]
node2 --> tail["tail"]
tail -.-> node2
node2 -.-> node1
node1 -.-> head
style head fill:#f9f,stroke:#333
style tail fill:#f9f,stroke:#333
head 节点是一个哨兵节点(不关联具体线程),每个等待的线程都有自己的 Node 节点,prev 和 next 指针构成了双向链表。
独占模式下的获取与释放
AQS 提供了 acquire 和 release 作为独占模式下获取和释放同步状态的入口。
acquire - 获取同步状态
1 | public final void acquire(int arg) { |
这个方法的执行流程如下:
- 调用
tryAcquire尝试获取同步状态(由子类实现) - 如果失败,调用
addWaiter将当前线程包装成 Node 节点加入等待队列 - 调用
acquireQueued让节点在队列中自旋等待 - 如果线程被中断,响应中断
整个过程可以看作:
flowchart TD
A[调用 acquire] --> B{tryAcquire
获取同步状态?}
B -->|成功| C[执行业务逻辑]
B -->|失败| D[addWaiter
构建Node加入队尾]
D --> E[acquireQueued
自旋等待]
E --> F{前驱节点
是 head?}
F -->|否| G[阻塞当前线程
等待前驱唤醒]
G --> E
F -->|是| H[再次 tryAcquire]
H -->|成功| I[设置为新 head
返回中断状态]
H -->|失败| G
I --> C
addWaiter - 入队
1 | private Node addWaiter(Node mode) { |
代码先尝试一次快速的 CAS 入队,如果失败则调用 enq 方法进行自旋入队,保证线程安全。
acquireQueued - 自旋等待
1 | final boolean acquireQueued(final Node node, int arg) { |
核心逻辑很简单:只有当前驱节点是 head 时才会尝试获取锁,获取成功则成为新的 head。如果获取失败,调用 shouldParkAfterFailedAcquire 检查是否应该挂起线程,如果前驱节点的 waitStatus 被设置为 SIGNAL(表示它会在释放锁时唤醒后继),则当前线程可以安全挂起。
release - 释放同步状态
1 | public final boolean release(int arg) { |
释放成功后会调用 unparkSuccessor 唤醒 head 的下一个等待线程。释放流程相对简单,主要是因为复杂的排队逻辑已经在 acquire 中处理了。
flowchart LR
A[持有锁的线程执行完毕] --> B[release]
B --> C{tryRelease
释放成功?}
C -->|否| D[直接返回 false]
C -->|是| E{head 不为 null
且 waitStatus != 0?}
E -->|否| F[返回 true
无等待线程]
E -->|是| G[unparkSuccessor
唤醒后继线程]
G --> H[后继线程继续 acquireQueued]
共享模式下的获取与释放
共享模式与独占模式的核心区别在于:共享模式下,当一个线程获取到同步状态后,可以同时通知后续的等待线程。
acquireShared
1 | public final void acquireShared(int arg) { |
tryAcquireShared 返回 int 值:
- 正数:获取成功,且后续线程也可以尝试获取
- 零:获取成功,但后续线程不能获取
- 负数:获取失败
独占 vs 共享对比
| 特性 | 独占模式 | 共享模式 |
|---|---|---|
| 同一时刻 | 只有一个线程持有 | 多个线程可以同时持有 |
| 典型实现 | ReentrantLock | Semaphore、CountDownLatch |
| 唤醒策略 | 只唤醒下一个节点 | 传播唤醒后续节点 |
| tryXxx 返回值 | boolean | int(正/零/负) |
基于 AQS 的实现
ReentrantLock
ReentrantLock 是 AQS 独占模式最典型的应用。它的核心是内部类 Sync 继承 AQS,并实现了 tryAcquire 和 tryRelease。
1 | // 非公平锁的 tryAcquire 实现 |
这里体现了两个关键点:
- 非公平性:新来的线程可以和队列中的线程竞争(先 CAS 尝试再入队)
- 可重入:同一个线程可以多次获取锁,state 递增累计
Semaphore
Semaphore(信号量)基于 AQS 的共享模式实现。它通过 permits(许可证数量)来限制同时访问资源的线程数。
1 | // 非公平信号量的 tryAcquireShared |
当 state 代表的剩余许可证不够时,返回负数,线程进入等待队列。
CountDownLatch
CountDownLatch 基于 AQS 共享模式实现,使用 state 代表计数器的初始值。countDown() 调用 releaseShared 递减 state;await() 调用 acquireShared 阻塞直到 state 为 0。
1 | // 内部 Sync 的 tryAcquireShared |
state 减到 0 时,tryReleaseShared 返回 true,AQS 会唤醒所有在 await 上等待的线程。
三种同步器对比
flowchart TD
subgraph AQS核心
state["state (volatile int)"]
queue["CLH FIFO 双向队列"]
end
subgraph 独占模式
ReentrantLock["ReentrantLock
state=0 无锁
state=1 有锁
state>1 重入次数"]
end
subgraph 共享模式
Semaphore["Semaphore
state=剩余许可证数"]
CountDownLatch["CountDownLatch
state=初始计数"]
end
AQS核心 --> ReentrantLock
AQS核心 --> Semaphore
AQS核心 --> CountDownLatch
总结
AQS 的设计体现了几个重要的编程理念:
- 模板方法模式:AQS 定义了获取/释放同步状态的骨架,子类只需实现
tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively等几个方法。 - 自旋 + CAS:入队和出队操作通过自旋 + CAS 保证线程安全,避免使用重量级锁。
- LockSupport:线程的阻塞和唤醒依赖
LockSupport.park()和unpark(),比wait/notify更灵活。 - 可扩展性:基于 AQS 可以方便地实现自定义同步器,如手写一个一次性门阀(类似于只有一次计数的 CountDownLatch)。
理解 AQS,本质上就理解了 Java 并发包中半数以上同步工具的实现原理,这也是为什么我们说 AQS 是 JUC 的基石。
文章作者:米兰
原始链接:https://blog.milanchen.site/posts/aqs.html
版权声明:转载请声明出处