AQS 是什么?它存在什么问题?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
框架设计理解:面试官不仅仅是想知道 AQS 是 "一个队列",更是想知道你是否理解它的 模板方法设计模式——AQS 定义了获取/释放的骨架流程,子类只需实现 "尝试获取" 和 "尝试释放" 的逻辑。
-
核心机制掌握:考察你是否清楚 AQS 的三大核心组件——
state状态变量、CLH 双向等待队列、独占/共享两种模式。这三块至少得讲透两块。 -
局限性认知:很多候选人只会夸 AQS,但说不出它的局限。能指出 AQS 不支持重入检测、不可中断性受限、不支持超时等待的早期版本等问题,说明你是真正在源码层面理解过。
核心答案
先甩结论:AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个抽象基类,它用 state 状态变量 + CLH 双向队列实现了一套通用的同步器框架。子类只需实现 "尝试获取/释放" 的逻辑,排队、阻塞、唤醒这些脏活累活 AQS 全包了。
AQS 的核心设计理念——模板方法模式:AQS 负责排队和线程调度,你只管定义 "什么算获取成功、什么算释放完成"。
| AQS 核心组件 | 作用 |
|---|---|
state(volatile int) |
同步状态,不同实现赋予不同含义 |
| CLH 双向队列 | 存放等待获取锁的线程 |
独占模式(Exclusive) |
同一时刻只有一个线程能获取(如 ReentrantLock) |
共享模式(Shared) |
多个线程可同时获取(如 Semaphore、CountDownLatch) |
深度解析
一、state——AQS 的 "状态机"
state 是 AQS 最核心的字段,用 volatile int 修饰,保证了多线程下的可见性。不同的子类赋予 state 不同的含义:
ReentrantLock中,state = 0表示无人持有,state = 1表示被占用,state = 2表示同一个线程重入了两次ReentrantReadWriteLock更巧妙,把一个int拆成两半用——高 16 位记录读锁数量,低 16 位记录写锁数量Semaphore中,state就是还剩多少个许可证CountDownLatch中,state就是还需要countDown()多少次
对 state 的修改全部通过 CAS 保证原子性,这也是 AQS 高性能的根基。
二、CLH 双向队列——线程排队的 "等候区"
当线程获取锁失败时,AQS 会把它包装成一个 Node 节点,塞进 CLH 双向队列里排队:
上图展示了 AQS 的 CLH 队列结构。几个关键点:
- 双向链表:每个
Node有prev和next指针,head 指向队首(通常是哨兵节点),tail 指向队尾 - 入队过程:新来的线程 CAS 把自己加到 tail 后面,成为新的 tail
- 哨兵节点:
head指向的节点是一个空壳(不绑定线程),真正的第一个等待线程是head.next waitStatus:记录每个节点的状态,最关键的是SIGNAL——表示 "我后面有人,我释放的时候记得唤醒它"
三、获取锁的完整流程(独占模式)
以 ReentrantLock 为例,一个线程获取锁的完整流程:
上面是独占模式下获取锁的完整链路。整个流程可以概括为三步:
- 快速尝试:先
tryAcquire(),用 CAS 抢一把state,抢到就完事了 - 入队等待:抢不到就把自己包装成
Node,CAS 加入 CLH 队列尾部 - 自旋 + 挂起:在队列里检查自己是不是队首,是的话再
tryAcquire()一次;不是的话通过LockSupport.park()挂起线程,等待前驱节点释放锁时唤醒
释放锁的流程就简单多了:state 减到 0,找到后继节点(head.next),调用 LockSupport.unpark(succNode.thread) 唤醒它。
四、模板方法——AQS 最优雅的设计
AQS 的源码读起来确实爽,Doug Lea 的设计功力真的服气。核心思想是 模板方法模式:
// AQS 定义的模板方法(骨架流程,子类不能重写)
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 子类实现:尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② AQS 负责:入队 + 排队 + 挂起
selfInterrupt();
}
// 子类需要重写的方法(只定义 "什么算获取成功")
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
acquire() 是 final 方法,定义了完整的获取流程。子类只需重写 tryAcquire() 来定义 "什么算获取成功" 和 tryRelease() 来定义 "什么算释放完成"。排队、阻塞、唤醒这些底层机制全部由 AQS 搞定。
这就是为什么 ReentrantLock、Semaphore、CountDownLatch 都基于 AQS,但行为完全不同——它们各自对 state 的含义和 tryAcquire 的判断逻辑有不同的实现。
五、AQS 存在什么问题?
AQS 设计得确实优雅,但它也不是没有局限:
1. 学习曲线陡峭
AQS 的源码不算长(约 2500 行),但极其精炼。acquire/release、acquireShared/releaseShared、Condition 队列、CANCELLED 节点清理……光是理清这些逻辑就要花不少时间。说实话,这块我当年也啃了好几遍才通透。
2. Condition 队列和同步队列的切换复杂
AQS 的 ConditionObject 实现了 Condition 接口,内部维护了一个 单向条件队列。调用 .await() 时,线程从同步队列转移到条件队列;调用 .signal() 时,又从条件队列转移回同步队列。两个队列之间的节点迁移逻辑非常容易搞混,也容易出 bug。
3. 取消节点清理(cancelAcquire)的边界情况
线程在等待过程中可能因为中断或超时被取消。AQS 需要把 CANCELLED 状态的节点从队列中摘除,但这个清理逻辑涉及前驱和后继指针的多次调整,边界条件很多。Doug Lea 本人在注释中也承认这块逻辑 "intricate"(错综复杂)。
4. 不支持非阻塞式获取的回调
AQS 的 tryAcquire() 返回的是 boolean,不支持异步回调。如果你需要 "获取失败时不阻塞,而是回调通知" 这种模式,AQS 原生不支持,需要自己在上层封装。
5. 共享模式的传播机制容易踩坑
共享模式下(如 Semaphore),一个线程释放后可能需要 级联唤醒 多个等待线程(setHeadAndPropagate)。这个 "传播" 逻辑在某些边界条件下可能导致多余唤醒或漏唤醒,Doug Lea 在 JDK 9 中专门修过这个 bug(JDK-8078836)。
面试高频追问
-
ReentrantLock的公平锁和非公平锁在 AQS 层面有什么区别?FairSync的tryAcquire()多了hasQueuedPredecessors()检查——如果队列里有人在等,你就不能插队。NonfairSync没这个检查,直接 CAS 抢。这就是之前 "公平锁和非公平锁的区别" 那篇里讲的那个hasQueuedPredecessors()。 -
AQS 为什么用 CLH 队列而不是普通队列?
CLH 队列的特点是每个节点只需要 自旋检查前驱节点的状态,而不需要全局同步。这在 NUMA 架构下性能更好,因为线程只需要关注自己前驱节点所在的缓存行,减少了跨 CPU 的缓存一致性开销。
-
CountDownLatch和CyclicBarrier有什么区别?CountDownLatch基于 AQS(state= 剩余次数),是一次性的,用完不能重置。CyclicBarrier不基于 AQS,内部用ReentrantLock+Condition实现,可以循环使用。
常见面试变体
- "AQS 的底层原理是什么?"
- "
ReentrantLock的底层是怎么实现的?" - "CLH 队列是什么?AQS 为什么用它?"
- "AQS 的
state是怎么用的?"
记忆口诀
AQS 三大件:state(状态)+ CLH 队列(排队)+ 独占/共享(模式)
模板方法:AQS 管排队阻塞唤醒,子类管 "什么算成功"
独占流程:tryAcquire 快抢 → 失败 addWaiter 入队 → acquireQueued 自旋/挂起
总结
AQS 是 Java 并发包的基石,用 state + CLH 双向队列 + 模板方法模式构建了一套通用的同步器框架。ReentrantLock、Semaphore、CountDownLatch 等常用并发工具都基于它实现。核心流程就是:CAS 抢 state → 失败入 CLH 队列 → 自旋或 park 挂起 → 前驱释放时 unpark 唤醒。面试时把 "三大件" 和 "独占获取流程" 讲清楚,再能指出它的局限性,这道题就是高分。
