AQS
1. AQS 到底是什么
AQS 全称是 AbstractQueuedSynchronizer。
它是一个 同步器框架,用来帮助开发者快速实现各种锁和同步组件。
你可以把它理解成:
AQS 提供了一套通用的“排队 + 竞争状态 + 阻塞唤醒”机制,具体这个同步器是锁、信号量、倒计时器,交给子类自己定义。
所以 AQS 干的事情主要有三件:
用一个 state 表示同步状态
用一个双向队列管理抢不到锁的线程
提供 acquire / release 这类模板方法,子类只要实现 tryXxx 即可
2. 为什么 AQS 这么重要
因为 Java 里很多并发工具底层都靠它:
ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLockConditionObject
也就是说,AQS 有点像一个 并发世界的脚手架。
你不需要每次都手搓:
谁抢锁成功
谁入队等待
谁该阻塞
谁该被唤醒
队列怎么维护
这些脏活累活 AQS 给你包了。
3. AQS 的设计思想
AQS 的思想很优雅,核心是 模板方法模式。
它把同步器拆成两层:
第一层:通用框架层
AQS 自己负责:
state状态管理等待队列管理
线程入队
线程挂起
park线程唤醒
unpark独占 / 共享模式的流程控制
第二层:子类自定义规则
子类只需要告诉 AQS:
当前这个同步器,怎么判断能不能拿到资源
当前这个同步器,怎么释放资源
比如这些方法:
1 | protected boolean tryAcquire(int arg) |
也就是说:
AQS 说,排队和调度我来做,资源能不能拿你来说。
这个分工特别妙。
4. AQS 最核心的两个东西
4.1 state,同步状态
AQS 里有一个非常关键的变量:
private volatile int state;
它表示同步状态。
这个状态的含义由子类决定。
不同组件里 state 的含义不同
ReentrantLock
0表示没锁1表示已被某线程持有大于
1表示重入次数
Semaphore
state表示剩余许可证数量
CountDownLatch
state表示剩余倒计时次数
ReentrantReadWriteLock
state高低位拆分,分别表示读锁和写锁状态
所以你要记住一句:
AQS 只提供 state 这个容器,具体语义由子类赋予。
4.2 CLH 变体双向等待队列
AQS 内部维护了一个 FIFO 双向链表队列,一般说它是基于 CLH queue 思想实现的变体。
它的作用是:
抢资源失败的线程进队
按顺序排队等待
前驱节点释放后,唤醒后继节点
队列节点大概长这样:
1 | static final class Node { |
几个关键字段:
prev:前驱节点next:后继节点thread:当前节点绑定的线程waitStatus:当前节点等待状态nextWaiter:用于区分独占/共享模式,或者条件队列
5. AQS 队列里的 Node 是什么
每个等待线程会被包装成一个 Node 节点。
你可以理解成:
线程抢锁失败
AQS 给它发一个号码牌
Node让它去队尾排队
等前面的人处理完,再轮到它
6. waitStatus 是什么
这个字段面试里也挺常见。
常见值有这些:
0:默认状态SIGNAL = -1:当前节点的后继需要被唤醒CANCELLED = 1:线程取消等待CONDITION = -2:节点在条件队列中PROPAGATE = -3:共享模式下传播唤醒
你不用死记所有值,先抓主线:
最重要的两个
SIGNAL:告诉前驱,释放时记得唤醒我后面的线程CANCELLED:这个节点废了,别管它了
7. AQS 的两种模式
AQS 支持两种资源获取模式:
7.1 独占模式 Exclusive
同一时刻只能一个线程拿到资源。
比如:
ReentrantLock
7.2 共享模式 Shared
同一时刻可以多个线程一起拿资源。
比如:
SemaphoreCountDownLatch的 await 唤醒传播ReentrantReadWriteLock的读锁
这个分类非常关键,因为 AQS 的很多方法都分两套:
acquire / release对应独占acquireShared / releaseShared对应共享
8. AQS 获取锁的整体流程
先讲最经典的 独占获取流程。
以 acquire(int arg) 为例。
8.1 第一阶段,先直接抢
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
1 | selfInterrupt(); |
看这段代码,AQS 的思路非常清晰:
第一步
先调用子类实现的 tryAcquire(arg)
成功,直接拿到资源,结束
失败,说明有竞争,进入队列流程
这就是典型的 先抢一次,失败再排队。
8.2 第二阶段,失败就入队
addWaiter(Node.EXCLUSIVE)
这一步会把当前线程封装成一个独占节点,插入等待队列尾部。
入队底层用的是 CAS 保证线程安全。
队列一般会有一个哨兵头节点 head,真正等待的线程从 head.next 开始。
8.3 第三阶段,入队后自旋等待
acquireQueued(node, arg)
这个方法很重要,它会让线程在队列里循环判断:
逻辑大致是
看看自己前驱是不是头节点
如果前驱是头节点,说明自己有资格再试一次抢锁
调用
tryAcquire成功则自己变成新 head
失败则决定是否阻塞
阻塞后等前驱释放时唤醒自己
这里的思想很妙:
只有排在队头的线程,才有资格再次抢资源。
这样可以减少无意义竞争。
9. 为什么要阻塞,不一直自旋吗
你知道吗,这就是 AQS 很聪明的地方 ❤️
CAS 那类乐观锁喜欢一直自旋。
AQS 的场景更复杂,如果竞争持续很久,一直空转会把 CPU 烧得很难看。
所以 AQS 采用的是:
短暂自旋检查
条件合适就 park 挂起
前驱释放时再 unpark 唤醒
这属于 自旋 + 阻塞 的结合体。
10. park / unpark 是什么
AQS 挂起线程用的是 LockSupport:
LockSupport.park()挂起当前线程LockSupport.unpark(thread)唤醒指定线程
你可以把它理解成并发包里的“精准停车和叫醒”。
比 wait/notify 更底层,也更灵活。
11. 独占模式完整流程图
你脑子里可以这样想:
线程进来
↓
tryAcquire 尝试抢资源
↓
成功 -> 直接返回
↓
失败
↓
封装成 Node 加入队尾
↓
循环判断:
前驱是不是 head?
是 -> 再次 tryAcquire
成功 -> 自己设为 head,出队
失败 -> park 挂起
否 -> park 挂起
↓
前驱释放资源时 unpark 后继
↓
被唤醒后继续循环尝试
12. AQS 释放锁的流程
独占释放是 release(int arg):
1 | public final boolean release(int arg) { |
if (h != null && h.waitStatus != 0)
1 | unparkSuccessor(h); |
流程是:
第一步
调用子类实现的 tryRelease(arg)
子类决定:
资源是否真的完全释放了
如果是可重入锁,是减计数还是彻底释放
第二步
如果确实释放成功,就唤醒 head 的后继节点
也就是:
当前持锁线程放手
叫醒排在最前面的等待线程来继续竞争
13. 为什么释放时唤醒的是后继节点
因为等待队列本质上是 FIFO。
AQS 想尽量保证:
- 排前面的先获得再次尝试的资格
注意是 先唤醒先尝试,并不代表 100% 绝对公平,除非具体实现就是公平锁。
14. 公平锁和非公平锁在 AQS 里怎么体现
这个很经典。
非公平锁
线程一上来就先直接抢,不太管队列里有没有人。
比如 ReentrantLock 默认就是非公平锁。
好处:
吞吐量高
少一次排队判断
坏处:
- 可能插队
公平锁
线程来时先看队列前面有没有人在等。
有的话,自己老实排队。
AQS 里通常通过 hasQueuedPredecessors() 来判断前面是否已有排队线程。
15. ReentrantLock 是怎么基于 AQS 实现的
这是理解 AQS 最好的例子。
ReentrantLock 内部有一个 Sync,它继承 AQS。
非公平锁 tryAcquire 逻辑核心思路
情况 1:state == 0
说明锁空闲,尝试 CAS 抢占:
CAS 成功,设置 owner 为当前线程
获取成功
情况 2:锁已被当前线程持有
说明发生重入:
state++
成功
情况 3:锁被别人持有
失败
进入 AQS 队列等待
所以 ReentrantLock 的重入本质就是:
同一线程再次获取锁时,发现 owner 还是自己,于是只增加 state 计数。
释放时则反过来:
state–
直到减到 0,才真正释放锁并唤醒后继
16. CountDownLatch 为什么也能用 AQS
这个点很多人第一次看会觉得神奇。
CountDownLatch 用的是 共享模式。
state 的含义
- 表示剩余计数
await()
调用 acquireSharedInterruptibly(1)
如果 state != 0,当前线程进队等待。
countDown()
调用 releaseShared(1)
把 state 减 1。
当减到 0 时,说明条件满足,唤醒所有等待线程。
所以它不是“抢锁”,它是在复用 AQS 的:
state 管理
共享队列唤醒机制
17. Semaphore 为什么能用 AQS
Semaphore 也走共享模式。
state 的含义
- 当前可用许可证数量
acquire()
尝试把 state 减掉一个许可数
成功,拿到许可证
失败,说明没资源,入队等待
release()
把 state 加回去,然后唤醒后继节点继续竞争
所以共享模式下,大家竞争的不是“唯一锁”,而是“若干份资源”。
18. 共享模式和独占模式的区别到底在哪
独占模式
同一时刻一个线程成功
成功后其他线程继续等
比如互斥锁
共享模式
一次可能多个线程都成功
一个线程成功后,可能还要继续传播唤醒后继
比如信号量、读锁
所以共享模式里有个很关键的概念:
传播 propagation
如果资源还有剩余,就可以继续唤醒后面节点。
这就是 PROPAGATE 状态出现的背景。
19. Condition 是什么,和 AQS 什么关系
这个点超重要。
Condition 是用来做 条件等待队列 的。
通常和 Lock 配合使用,类似 Object.wait/notify 的升级版。
比如:
1 | lock.lock(); |
// do something
1 | } finally { |
AQS 里的 ConditionObject 是其内部类。
20. Condition 的本质
AQS 其实维护了两类队列:
1. 同步队列
抢锁失败的线程排这里
2. 条件队列
调用 await() 后,线程先进条件队列
条件队列里的线程特点
它们在等某个条件成立
暂时不参与抢锁
当别的线程调用 signal() 时:
条件队列头节点被转移到同步队列
然后去重新参与锁竞争
这个转移过程是 Condition 的精髓。
21. await 和 signal 流程
await()
线程当前必须先持有锁,然后:
把当前线程包装成条件节点,加入条件队列
释放当前持有的锁
挂起自己
等待被 signal
被转移到同步队列
重新竞争锁
拿到锁后从 await 返回
signal()
当前线程必须持有锁,然后:
从条件队列取出一个节点
转移到同步队列
后续让它参与正常的抢锁流程
所以:
await 不是直接唤醒后立刻执行,而是先回到同步队列重新抢锁。
这个非常容易考。
22. AQS 为什么要用双向链表
因为它需要高效处理这些操作:
从尾部入队
找前驱节点
取消节点时跳过失效节点
唤醒后继节点
单链表做这些会麻烦很多,尤其取消和回溯处理。
23. AQS 为什么 head 节点通常不存真实线程
因为 head 更多像一个 哨兵节点。
当一个线程成功获取资源后,它会把自己所在节点设为新的 head。
原来的头节点会被丢弃。
head 的主要作用是:
标识队列起点
简化队列逻辑
让
head.next成为实际等待的第一个线程
24. AQS 和 synchronized 的区别
这个面试也常问。
synchronized
JVM 层面的关键字
底层依赖对象监视器
monitor使用更简单
自动释放锁
支持可重入
条件等待靠
wait/notify
AQS
JDK 层面的同步器框架
基于 CAS + volatile + CLH 队列 + park/unpark
扩展性更强
可以实现公平锁、非公平锁、共享锁、条件队列等复杂语义
你可以这么理解:
synchronized更像现成家具,AQS 更像高质量工具箱。
25. AQS 底层靠哪些技术拼起来
这个可以当总结图记住:
volatile
保证 state、head、tail 等变量的可见性
CAS
保证入队、出队、修改 state 时的原子性
CLH 变体队列
管理等待线程顺序
LockSupport
实现线程阻塞和唤醒
这四个点一起组成了 AQS 的骨架。
26. AQS 常见源码级核心方法
你不用全背,先认脸熟。
独占相关
acquire(int arg)release(int arg)tryAcquire(int arg)tryRelease(int arg)
共享相关
acquireShared(int arg)releaseShared(int arg)tryAcquireShared(int arg)tryReleaseShared(int arg)
队列相关
addWaiter(Node mode)enq(Node node)acquireQueued(Node node, int arg)unparkSuccessor(Node node)
条件队列相关
await()signal()signalAll()
27. 为什么说 AQS 是模板方法模式
因为它把主流程写死了:
获取失败怎么办
入队怎么办
挂起怎么办
释放后唤醒谁
这些都由父类 AQS 控制。
子类只负责覆盖几个钩子方法:
tryAcquiretryReleasetryAcquireSharedtryReleaseShared
这就是非常典型的模板方法。
28. AQS 的一个极简伪代码理解版
获取独占锁
1 | public final void acquire(int arg) { |
释放独占锁
1 | public final boolean release(int arg) { |
你看到这里应该有感觉了:
AQS 的世界,其实一直在反复做这件事:
抢资源,失败入队,挂起等待,被唤醒后再抢。
29. 面试最容易问的高频点
AQS 是什么
AQS 是一个同步器框架,通过 state 和 FIFO 等待队列来实现线程同步,很多锁和同步组件都基于它实现。
AQS 的核心组成
volatile state
CLH 变体双向队列
CAS
LockSupport
AQS 支持哪两种模式
独占模式
共享模式
AQS 和 ReentrantLock 的关系
ReentrantLock 内部的 Sync 继承 AQS,通过重写 tryAcquire 和 tryRelease 实现可重入互斥锁。
Condition 和 AQS 的关系
ConditionObject 是 AQS 的内部条件队列实现,await 会进入条件队列,signal 会把节点转移到同步队列重新竞争锁。
公平锁和非公平锁区别
公平锁会先检查队列中是否已有前驱节点,非公平锁会直接尝试 CAS 抢占。
30. 你可以直接背的面试版回答
版本一,正常详细版
AQS 全称 AbstractQueuedSynchronizer,是 JUC 中用来构建锁和同步器的基础框架。它的核心思想是用一个 volatile 修饰的 state 表示同步状态,用一个 FIFO 双向等待队列管理获取同步状态失败的线程,并结合 CAS 和 LockSupport 实现原子更新、线程阻塞与唤醒。AQS 支持独占和共享两种模式。像 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都是基于 AQS 实现的。AQS 本身定义了 acquire、release 等模板方法,子类只需要重写 tryAcquire、tryRelease 等方法来自定义资源获取和释放规则。
版本二,简洁版
AQS 就是一个同步器脚手架。它负责排队、阻塞、唤醒和状态管理,子类只负责定义怎样算获取成功、怎样算释放成功。底层核心是 state、CLH 队列、CAS 和 park/unpark。
31. 学 AQS 时最容易晕的几个点
点 1:AQS 不是锁
AQS 是“造锁框架”,锁是它的子类实现。
点 2:state 不是固定表示锁数量
state 是通用同步状态,含义取决于子类。
点 3:入队后不会一直抢
只有接近队头的线程才有资格重点尝试,其他线程会 park。
点 4:signal 不是立刻执行
signal 只是把条件队列的节点转移回同步队列,后面还要重新抢锁。
点 5:共享模式会传播
一个线程成功后,可能继续唤醒后继,让多个线程一起通过。
32. 一张脑内总图
你可以把 AQS 想成这个模型:
├─ state:资源状态
├─ 同步队列:抢资源失败的线程排队
├─ 条件队列:await 的线程暂存
├─ acquire/release:主流程模板
├─ tryAcquire/tryRelease:子类自定义规则
├─ CAS:改状态、入队时保证原子性
└─ park/unpark:线程挂起与唤醒
33. 你当前阶段最该掌握的主线
Gilbert,现阶段你先把这 6 句拿稳,就已经很能打了:
AQS 是同步器框架,不是具体锁
核心变量是 volatile state
抢不到资源的线程会进入 FIFO 双向队列
独占模式常见于 ReentrantLock,共享模式常见于 Semaphore、CountDownLatch
底层依赖 CAS + CLH 队列 + LockSupport
Condition 有独立条件队列,被 signal 后会回到同步队列重新抢锁