并发06:AQS框架
围绕 ReentrantLock、Semaphore、CountDownLatch 背后的 AbstractQueuedSynchronizer,系统理解 state、CLH 队列、acquire/release 流程和自定义同步器的设计思想。
AQS 是什么
AQS,全名 AbstractQueuedSynchronizer,不是业务代码里直接拿来用的锁,而是一套构建同步器的基础框架。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 背后都能看到它的影子。
它要解决的问题很朴素:很多并发工具都需要”抢资源、抢不到就排队、资源释放后再唤醒”。如果每个工具都自己写一套等待队列、线程挂起和唤醒逻辑,成本很高,也容易错。
AQS 把通用部分抽出来:用一个 state 表示同步状态,用一个 FIFO 队列管理等待线程,再把”怎样算获取成功”留给具体同步器实现。
三个核心对象
理解 AQS 可以先抓三件事:
| 对象 | 作用 | 示例 |
|---|---|---|
| state | 同步状态 | ReentrantLock 中表示重入次数,Semaphore 中表示许可数 |
| 等待队列 | 管理等待线程 | CLH 双向队列,获取失败的线程入队等待 |
| 模板方法 | 定义获取/释放流程 | acquire、release 等方法 |
acquire 获取流程
以 ReentrantLock 为例,获取锁的流程:
- CAS 尝试获取:如果
state == 0,CAS 改成 1,记录 owner,获取成功 - 重入判断:如果当前线程已经是 owner,
state加 1 - 入队等待:获取失败,线程进入 AQS 队列,通过
LockSupport.park挂起
boolean tryAcquire(int arg) {
if (state == 0 && compareAndSetState(0, arg)) {
owner = currentThread;
return true;
}
if (owner == currentThread) {
state += arg;
return true;
}
return false;
}
队列节点状态
AQS 队列不是简单链表。节点里会记录等待状态,用来表达:
- 后继线程是否需要被唤醒
- 当前节点是否取消
- 是否在条件队列里等待
线程入队后不会立刻挂起,它会先判断前驱是不是头节点,如果是,就再尝试一次获取。这样能减少不必要的挂起和唤醒成本。
线程被唤醒后也不代表一定拿到锁。唤醒只是给它一次继续竞争的机会。它还要重新执行 tryAcquire,成功才会把自己设置成新的头节点。
公平锁和非公平锁
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 非公平锁 | 新线程先尝试直接 CAS 抢锁,可能插队 | 高频短临界区,追求高吞吐 |
| 公平锁 | 先看队列是否有前驱,有人等就排队 | 核心交易状态更新,追求等待有序 |
公平不是绝对更好。核心交易状态更新也许更在意等待有序;高频短临界区也许更在意吞吐。项目里选公平锁,要有业务理由,而不是因为”公平”这个词听起来更稳。
Condition 条件队列
Condition 容易被误解成”更灵活的 wait/notify”。它真正做的是把等待条件从同步队列里拆出去:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
线程调用 await() 时,会释放当前锁,进入条件队列;其他线程修改条件后调用 signal(),等待节点才会从条件队列转移回 AQS 同步队列,重新竞争锁。
有界队列、连接池、对象池这类结构,经常需要两个条件:队列非空才能取,队列未满才能放。Condition 的价值就在这里:不同条件可以有不同等待队列,避免所有线程都被一个粗粒度通知唤醒。
共享模式和独占模式
AQS 支持两种模式:
| 模式 | 特点 | 示例 |
|---|---|---|
| 独占模式 | 同一时刻只有一个线程获取成功 | ReentrantLock |
| 共享模式 | 多个线程可以同时获取成功 | Semaphore、CountDownLatch |
这也是为什么不同工具看起来行为差很多,但底层模型相似。差异主要在 state 怎么解释,以及获取成功后是否允许继续传播唤醒。
常见误区
误区一:把 AQS 当成普通队列
AQS 队列不是为了存任务,而是为了管理等待同步状态的线程。队列里排的是”谁有资格下一次尝试获取”,不是业务任务。
误区二:以为入队顺序就一定等于公平
AQS 可以支持公平锁,也可以支持非公平锁。非公平锁允许新来的线程先尝试抢一次,抢到了就不进队列。
误区三:只记源码细节,不理解状态含义
学 AQS 最重要的是看具体同步器如何解释 state。同一个字段,在锁、信号量和门闩里表达的是完全不同的业务语义。
项目里为什么要懂 AQS
大多数业务系统不需要自己继承 AQS 写同步器,但理解它能帮你判断并发工具的边界:
CountDownLatch只能倒数一次,归零后不能重置;需要重复使用看CyclicBarrier或PhaserSemaphore控制的是许可数,不等于线程池,适合限制同时访问某个资源的并发量ReentrantLock的公平锁能减少饥饿,但可能牺牲吞吐
线上排查时,如果线程栈里大量出现 LockSupport.park,就知道线程在等待同步条件。下一步要找的是谁持有锁、谁没有释放许可、哪个条件没有被 signal。
理解方式
看到一个 AQS 工具,可以问三句话:
state表示什么?- 获取失败的线程什么时候入队,什么时候被唤醒?
- 释放动作会唤醒一个线程,还是传播给多个线程?
这三句话比背完整源码更有用。AQS 的价值不是让我们炫技写锁,而是让我们看懂 Java 并发工具背后共同的排队和唤醒模型。