- Java并发之AQS详解
- 1. AQS 是什么?
- 2. 核心原理
- 2.1 一个状态:
state
- 2.2 一个队列:CLH 变种队列
- 2.3 一套模板方法:获取与释放
- 2.1 一个状态:
- 3. 工作流程(以 ReentrantLock 的独占模式为例)
- 3.1 获取锁 (
lock()
->acquire(1)
) - 3.2 释放锁 (
unlock()
->release(1)
)
- 3.1 获取锁 (
- 4. 主要同步器实现
- 5. Synchronized 、Lock、Condition、AQS
- 6. 总结与要点
Java并发之AQS详解
Java并发之AQS详解 - waterystone - 博客园
1. AQS 是什么?
AQS,全称 AbstractQueuedSynchronizer
(抽象队列同步器),是 Java 并发包 java.util.concurrent.locks
下的一个核心基础框架。
它的主要作用是为构建锁和同步器(如 Semaphore、CountDownLatch 等)提供一个底层的、通用的同步机制。你可以把它想象成一个“同步器的骨架”,它帮你处理了复杂的线程排队、阻塞、唤醒等底层细节,你只需要按需实现一些关键的方法,就能定制出自己的同步工具。
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
一句话总结:AQS 是 JUC 锁的“大脑”,它管理着线程的排队、阻塞和唤醒。
2. 核心原理
AQS 的核心原理可以概括为三部分:一个状态、一个队列 和 一套模板方法。
2.1 一个状态:state
AQS 内部维护了一个关键的 volatile
整型变量,名为 state
。
- 作用:
state
表示共享资源的状态。具体含义由子类定义,非常灵活。 - 示例:
- 在
ReentrantLock
中,state=0
表示锁空闲,state=1
表示锁被占用,state>1
表示锁被同一个线程重入。 - 在
Semaphore
中,state
表示可用的许可证数量。 - 在
CountDownLatch
中,state
表示倒计数的数值。
- 在
对 state
的修改使用 CAS(Compare-And-Swap)操作来保证原子性,这是实现非阻塞同步的基础。
2.2 一个队列:CLH 变种队列
AQS 内部维护了一个双向的 FIFO 线程等待队列(通常被称为 CLH 队列的变种)。当线程获取资源失败时,AQS 会将该线程以及其等待状态(如是否独占)包装成一个 Node 节点,并将其加入到队列尾部,然后阻塞该线程。
- Node 节点:包含了线程引用、等待状态(如
CANCELLED
,SIGNAL
等)、前驱节点(prev
)和后继节点(next
)。 - 作用:这个队列是所有未能立即获取到资源的线程的“等候室”,它严格保证了等待的公平性(FIFO)。
2.3 一套模板方法:获取与释放
AQS 使用了 模板方法设计模式。它定义了一套顶层的获取和释放资源的流程(如 acquire(int arg)
和 release(int arg)
),而将一些关键的是否成功、如何修改状态的判断留给子类去实现。
子类需要重写的关键方法:
protected boolean tryAcquire(int arg)
:尝试以独占方式获取资源。成功则返回 true,失败则返回 false。protected boolean tryRelease(int arg)
:尝试以独占方式释放资源。成功则返回 true,失败则返回 false。protected int tryAcquireShared(int arg)
:尝试以共享方式获取资源。负数表示失败;0 表示成功,但无剩余可用资源;正数表示成功,且有剩余资源。protected boolean tryReleaseShared(int arg)
:尝试以共享方式释放资源。
AQS 提供的核心模板方法(供使用者调用):
- 独占模式:
acquire(int arg)
:获取资源,如果失败则进入队列等待。此过程不可中断。release(int arg)
:释放资源,成功后唤醒队列中下一个等待的线程。
- 共享模式:
acquireShared(int arg)
releaseShared(int arg)
3. 工作流程(以 ReentrantLock 的独占模式为例)
我们以锁的获取和释放来看 AQS 是如何工作的。
3.1 获取锁 (lock()
-> acquire(1)
)
- 线程 A 调用
lock()
。 lock()
内部会调用 AQS 的acquire(1)
。acquire(1)
的流程如下:tryAcquire(1)
:子类(ReentrantLock 的 Sync)实现此方法。检查state
:- 如果
state == 0
(锁空闲),则通过 CAS 将其设为 1,并设置当前线程为独占线程,返回true
。流程结束,线程 A 获得锁。 - 如果
state != 0
,但独占线程就是线程 A 自己(锁重入),则将state
加 1,返回true
。 - 否则,返回
false
。
- 如果
- 如果
tryAcquire
返回false
(获取失败),则调用addWaiter(Node.EXCLUSIVE)
。将线程 A 包装成一个独占模式的 Node 节点,并采用 CAS 方式安全地插入到等待队列的尾部。 - 接着调用
acquireQueued(...)
。这个方法是核心中的核心:- 它会让节点自旋地尝试获取锁(在它即将成为队首时)。
- 如果还是失败,则会判断是否应该阻塞自己(通过前驱节点的
waitStatus
)。 - 最终,通过
LockSupport.park()
将线程 A 挂起(阻塞)。
3.2 释放锁 (unlock()
-> release(1)
)
- 线程 A 调用
unlock()
。 unlock()
内部会调用 AQS 的release(1)
。release(1)
的流程如下:tryRelease(1)
:子类实现此方法。将state
减 1。如果state
减到 0,表示锁完全释放,清空独占线程,返回true
。- 如果
tryRelease
返回true
,则它会找到等待队列中的头节点(head)。 - 如果头节点不为空且其状态有效,则调用
unparkSuccessor(Node node)
。 - 这个方法会使用
LockSupport.unpark(thread)
唤醒 头节点后继节点中第一个未被取消的线程(假设是线程 B)。
- 线程 B 被唤醒后,会从之前在
acquireQueued
中被park()
的地方继续执行。 - 线程 B 会再次自旋尝试
tryAcquire
。此时锁已被线程 A 释放,所以线程 B 有很大概率成功获取到锁,然后将自己设置为新的头节点,继续执行。
4. 主要同步器实现
AQS 是 JUC 中众多同步工具的基础:
- ReentrantLock:独占锁,使用 AQS 的独占模式。
- ReentrantReadWriteLock:读写锁。其读锁使用共享模式,写锁使用独占模式。
- Semaphore:信号量,使用 AQS 的共享模式。
- CountDownLatch:倒计时器,使用 AQS 的共享模式。
state
初始化为计数。 - CyclicBarrier:循环栅栏(其底层使用了 ReentrantLock 和 Condition,而 Condition 的实现也依赖于 AQS)。
- ThreadPoolExecutor:线程池中的 Worker 类(工作线程)也使用了 AQS 来实现独占锁,用于判断线程是否空闲。
5. Synchronized 、Lock、Condition、AQS
AQS 使用 CAS + volatile state + CLH队列,比传统的 synchronized 有更好的并发性能。
- ✅
Lock
替代原始的synchronized
- ✅
Condition
替代原始的wait/notify
- ✅ 主流的
Lock
和Condition
实现都是基于 AQS
synchronized 和 Lock (基于AQS)
原始方式 (JDK 1.0+) 现代方式 (JDK 5+) synchronized → Lock (基于AQS) wait/notify → Condition (基于AQS)
// 原始的 wait/notify
synchronized (lock) {while (!condition) {lock.wait(); // 所有等待者混在一起}// ...lock.notifyAll(); // 唤醒所有等待者
}// 现代的 Condition
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();lock.lock();
try {while (!condition) {condition.await(); // 只等待特定条件}// ...condition.signal(); // 只唤醒一个等待此条件的线程
} finally {lock.unlock();
}// 现代的 Condition Lock 提供更多功能if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁,带超时try {// ...} finally {lock.unlock();}
}
// 锁中断
lock.lockInterruptibly(); // 可中断的锁获取Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列未满条件
Condition notEmpty = lock.newCondition(); // 队列非空条件
// 生产者只等待 notFull 条件
// 消费者只等待 notEmpty 条件
// 互不干扰!
6. 总结与要点
- 定位:AQS 是构建锁和同步器的框架,不是直接给业务开发者使用的类。
- 核心机制:通过一个
volatile
的state
表示状态,一个 FIFO 队列管理等待线程,一套 CAS 操作保证状态更新的原子性。 - 设计模式:模板方法模式。使用者继承 AQS 并重写指定方法,将 AQS 组合在自定义同步组件的实现中。
- 两种模式:独占模式(一次只有一个线程能执行,如 ReentrantLock)和共享模式(多个线程可同时执行,如 Semaphore/CountDownLatch)。
- 重要性:理解了 AQS,就理解了 JUC 包中大部分同步工具的实现原理,是 Java 并发编程进阶的必经之路。
通过 AQS,Java 提供了一种高效、安全且可扩展的方式来构建复杂的同步结构,极大地简化了并发编程的难度。