第54篇:AQS抽象队列同步器(2026版)
📌 系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第53篇:volatile与JMM |
➡️ 下一篇:第55篇:线程池ThreadPoolExecutor
🗺️ 本文阅读地图(3 分钟速览)
第53篇搞懂了 JMM 和 volatile,本篇深入 JUC 并发体系的基石——AQS。掌握了 AQS,ReentrantLock、Semaphore、CountDownLatch 就全部打通了:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| AQS 是什么 | 为什么说它是 JUC 的基石? | JUC 中所有锁和同步器(ReentrantLock、Semaphore、CountDownLatch 等)底层都基于 AQS |
| 核心三要素 | AQS 由什么构成? | volatile int state(同步状态)+ CLH 双向队列(等待队列)+ 模板方法模式(控制框架) |
| 独占模式 | ReentrantLock 怎么实现? | state 表示锁被占用次数,0=空闲,>0=被持有或重入 |
| 共享模式 | Semaphore/CountDownLatch 怎么实现? | state 表示剩余许可数/剩余计数,多个线程可同时获取 |
| 面试最爱问 | 高频考点有哪些? | 见文末 面试 小节 |
文章目录
一、核心知识点
AQS 是什么?
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 java.util.concurrent 包(JUC)的底层基石——它是一个抽象类,为构建锁和同步器提供了通用框架。
核心设计思想:
AQS 的目标不是“提供一把具体的锁”,而是提供一个可复用的同步器框架——把“排队、阻塞、唤醒”这套通用流程封装好,子类只需实现“能不能获取同步状态”这个核心判断逻辑。
三大核心要素:
volatile int state:同步状态,用 CAS 原子更新- CLH 双向队列(FIFO):存储等待获取同步状态的线程
- 模板方法模式:定义获取/释放同步状态的骨架,子类只需实现
tryAcquire/tryRelease等方法
两种同步模式:
| 模式 | 特点 | 典型实现 |
|---|---|---|
| 独占模式(Exclusive) | 同一时刻只有一个线程能获取同步状态 | ReentrantLock |
| 共享模式(Shared) | 同一时刻多个线程可以同时获取同步状态 | Semaphore、CountDownLatch、ReentrantReadWriteLock(读锁) |
二、生活类比:从“餐厅排队叫号”到“VIP 通道”

AQS 就像一个餐厅的“排队叫号系统”:
state(同步状态):餐厅的空桌数量——独占模式下只有 1 张桌子(0=全满,1=空闲);共享模式下有多张桌子(state=剩余空桌数)。- CLH 队列:门口排队等位的顾客队伍(FIFO,先来后到)。
- 模板方法:餐厅统一规定“排队、叫号、入座”的流程,但每个分店可以决定“什么情况下叫号”(子类实现
tryAcquire逻辑)。
独占模式(ReentrantLock)就像“VIP 包间”:
- 包间只有 1 间,一次只能进 1 个人(state=0/1)。
- 同一个 VIP 客人可以多次进入(可重入,state 累加)。
- 其他客人在门口排队,包间客人出来后才让下一个进。
共享模式(Semaphore)就像“公共停车场”:
- 停车场有 N 个车位(state=N)。
- 每来一辆车占一个车位(state-1),车位满了就排队。
- 每走一辆车释放一个车位(state+1),唤醒排队车辆。
- 多辆车可以同时进出,互不影响。
三、AQS 核心三要素
3.1 state:同步状态
// AQS 核心属性
private volatile int state; // 同步状态,volatile 保证可见性
state 是 AQS 的“大脑”——所有同步逻辑都围绕它展开:
| 场景 | state 含义 |
|---|---|
| 独占模式(ReentrantLock) | 0=锁空闲,1=锁被占用,>1=同一线程重入次数 |
| 共享模式(Semaphore) | 剩余许可数量 |
| 共享模式(CountDownLatch) | 剩余计数(未完成线程数) |
3.2 CLH 队列:等待线程的“排队系统”
AQS 内部维护一个 CLH 双向队列(Craig, Landin, and Hagersten 队列),用于管理等待获取同步状态的线程。
队列结构:

Node 的状态(waitStatus):
| 状态 | 值 | 含义 |
|---|---|---|
| CANCELLED | 1 | 线程已取消等待(超时或被中断) |
| SIGNAL | -1 | 后继线程需要被唤醒(当前节点释放锁时要唤醒下一个) |
| CONDITION | -2 | 线程在 Condition 上等待 |
| PROPAGATE | -3 | 共享模式下需要向后传播(释放共享锁时通知所有后继) |
| 0 | 0 | 初始状态 |
入队流程:
- 线程获取同步状态失败
- 包装成 Node 节点
- CAS 设置到队尾
- 进入自旋或阻塞等待
3.3 模板方法模式
AQS 使用模板方法模式,定义了获取/释放同步状态的骨架,子类只需实现特定方法:
| 子类需实现的方法 | 说明 |
|---|---|
tryAcquire(int arg) | 独占式尝试获取同步状态 |
tryRelease(int arg) | 独占式尝试释放同步状态 |
tryAcquireShared(int arg) | 共享式尝试获取同步状态 |
tryReleaseShared(int arg) | 共享式尝试释放同步状态 |
isHeldExclusively() | 当前线程是否独占持有同步状态 |
AQS 提供的模板方法(子类直接调用):
acquire(int arg):独占式获取锁(模板方法)release(int arg):独占式释放锁(模板方法)acquireShared(int arg):共享式获取锁(模板方法)releaseShared(int arg):共享式释放锁(模板方法)

四、独占模式:以 ReentrantLock 为例
独占模式下,同一时刻只有一个线程能持有同步状态。
4.1 加锁流程(acquire)
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 子类实现:尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② 获取失败→入队
selfInterrupt(); // ③ 中断当前线程
}
完整流程(以非公平锁为例):

4.2 解锁流程(release)
public final boolean release(int arg) {
if (tryRelease(arg)) { // ① 子类实现:尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // ② 唤醒后继线程
return true;
}
return false;
}
4.3 ReentrantLock 如何实现可重入?
// ReentrantLock 的 Sync 子类(非公平锁)的 tryAcquire 实现(简化)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 锁空闲 → CAS 抢锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 当前线程已持有锁 → 重入!state++
int nextc = c + acquires;
setState(nextc);
return true;
}
return false; // 其他线程持有锁 → 获取失败
}
可重入的关键:判断当前线程是否已经是锁的持有者——如果是,直接 state++,无需再次竞争。
五、共享模式:以 Semaphore 和 CountDownLatch 为例
共享模式下,多个线程可以同时持有同步状态,state 的值代表剩余可用资源数。
5.1 Semaphore(信号量)——控制并发数
Semaphore 的 state 代表剩余许可数量:
acquire():state - 1,如果 state > 0 则成功,否则入队等待release():state + 1,唤醒等待线程
适用场景:限流——同一时刻最多 N 个线程访问某资源。
5.2 CountDownLatch(门栓)——等待所有线程完成
CountDownLatch 的 state 代表剩余计数(需要等待完成的线程数):
countDown():state - 1,当 state == 0 时唤醒所有等待线程await():如果 state != 0,当前线程阻塞等待
适用场景:主线程等待多个子任务完成后继续执行。
5.3 共享模式获取流程(acquireShared)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // ① 子类实现:尝试获取共享锁
doAcquireShared(arg); // ② 获取失败→入队
}
共享模式与独占模式的区别:
- 独占模式:
tryAcquire返回 boolean(成功/失败) - 共享模式:
tryAcquireShared返回 int(正数=还有剩余资源,0=刚好获取完,负数=失败)
六、AQS 在 JUC 中的全景图

七、生产级避坑清单
✅ AQS 与同步器使用避坑指南
1. 不要直接使用 AQS → 用它的子类(ReentrantLock、Semaphore、CountDownLatch)
2. CountDownLatch 计数器不能重置 → 需要重置用 CyclicBarrier
3. Semaphore 获取许可后记得释放 → try-finally 保证 release()
4. 公平锁 vs 非公平锁:公平锁保证 FIFO,吞吐量更低;非公平锁允许插队,吞吐量更高
5. ReentrantLock 必须在 finally 中 unlock() → 否则锁永不释放
6. tryLock() 带超时 → 获取失败可以做降级或重试,避免线程永久阻塞
7. Condition.await() 必须在 lock() 后调用 → 否则抛 IllegalMonitorStateException
八、面试高频考点
Q1:AQS 是什么?核心思想是什么?
AQS 是 AbstractQueuedSynchronizer 的缩写,JUC 并发包的底层基石。核心思想是:用一个
volatile int state表示同步状态,用一个 FIFO 双向队列(CLH 队列)管理等待线程,通过模板方法模式让子类只需实现tryAcquire/tryRelease等少量方法,就能构建自定义同步器。
Q2:AQS 支持哪两种模式?分别对应哪些实现?
独占模式(Exclusive):同一时刻只有一个线程能获取同步状态,典型实现是 ReentrantLock。共享模式(Shared):同一时刻多个线程可以同时获取同步状态,典型实现是 Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁。
Q3:ReentrantLock 的可重入性是如何实现的?
在
tryAcquire方法中,先判断 state 是否为 0(锁空闲),如果是则 CAS 抢锁;如果 state > 0,检查当前线程是否已经是锁的持有者,如果是则直接state++(可重入),如果不是则获取失败。解锁时每次state--,直到 state 归零才真正释放锁。
Q4:AQS 的 CLH 队列是什么?
CLH 队列是一个 FIFO 双向链表,用于存储等待获取同步状态的线程。每个线程被封装成 Node 节点,包含线程引用、等待状态(waitStatus)、前驱和后继指针。获取同步状态失败的线程会入队等待,释放锁时唤醒队首线程。
Q5:CountDownLatch 和 Semaphore 的区别?
CountDownLatch:state 表示倒计数,
countDown()减 1,state 归零时唤醒所有等待线程,计数器不能重置。Semaphore:state 表示剩余许可数,acquire()减 1,release()加 1,许可数可以动态增减,用于限流控制并发访问数。
Q6:AQS 如何避免 ABA 问题?
AQS 使用 CAS 操作更新 state,CAS 本身存在 ABA 问题,但 AQS 的 state 是整型计数器(单调增/减),ABA 问题不会影响锁的正确性——state 的值变化代表着“锁被释放了又被获取”,对于同步语义来说,只要最终状态正确即可。需要 ABA 敏感的原子操作应使用
AtomicStampedReference。
面试官追问陷阱(加分题)
追问1:“tryAcquire 和 acquire 的区别是什么?”
👉
tryAcquire是子类需要实现的具体逻辑(尝试获取同步状态,返回 boolean),由子类根据 state 判断能否获取。acquire是 AQS 提供的模板方法,封装了“尝试获取→失败入队→阻塞等待”的完整流程,子类不能重写。
追问2:“AQS 的共享模式和独占模式在队列处理上有什么区别?”
👉 独占模式释放锁时只唤醒一个后继线程(
unparkSuccessor)。共享模式释放锁时,会传播唤醒——如果当前节点的waitStatus为PROPAGATE,会继续唤醒后继节点,让多个等待线程同时获取共享锁。
追问3:“ReentrantLock 的公平锁和非公平锁在 AQS 层面有什么区别?”
👉 非公平锁的
tryAcquire中:CAS 抢锁前不检查 CLH 队列中是否有等待线程,直接尝试 CAS——这就是“插队”。公平锁的tryAcquire中:如果 CLH 队列中有等待线程,当前线程直接失败入队,不抢锁,保证 FIFO 顺序。其他流程(入队、阻塞、唤醒)完全一致。
九、练习题
-
分析题:AQS 的 CLH 队列为什么需要设计成双向链表而不是单向链表?
💡 思路:需要支持取消等待(CANCELLED 节点出队),单向链表无法从后往前删除;同时 Condition 等待队列的节点转移时需要前驱指针。
-
源码题:阅读 ReentrantLock 的非公平锁
tryAcquire源码,画出加锁失败时线程入队和阻塞的完整流程。 -
场景设计:某系统需要对数据库连接池做限流,最多同时允许 10 个线程获取连接。用 Semaphore 如何实现?
💡 思路:
Semaphore semaphore = new Semaphore(10);获取连接前semaphore.acquire(),归还连接时semaphore.release(),用 try-finally 保证释放。
📊 你的学习进度
- 当前:第54篇 / 共108篇 · 进阶篇:并发编程与JUC详解(第51~60篇)
- ✅ 已完成:基础篇44篇 + 第45~54篇
- 📖 正在学:第54篇
- ⏳ 待学习:第55~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀 下一篇:《第55篇:线程池ThreadPoolExecutor》
内容简介:七大参数深度解析、四种拒绝策略、任务队列选型、动态调优、
Executors工厂方法的陷阱(无界队列 OOM)、实际生产案例。
👉 多线程专题持续深入,拿下线程池核心!
📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
&spm=1001.2101.3001.5002&articleId=162748323&d=1&t=3&u=b58310780c1d4da487071cac829ae71f)
203

被折叠的 条评论
为什么被折叠?



