AQS
AbstractQueuedSynchronizer(AQS)是实现阻塞锁和同步器的框架。子类规定资源怎样获取和释放,AQS 处理获取失败后的排队与等待。本文以 Java 17/21 为准,从同步状态入手,再说明等待队列、独占与共享模式。最后解释取消、条件等待和线程唤醒的关系。
1. 面试核心
先记住这几条,再读正文- 同步状态。 AQS 用一个
int state表示同步状态,子类决定它代表重入次数、剩余计数还是可用许可。 - 排队与唤醒。 AQS 在子类尝试获取失败后管理排队、阻塞和取消,资源释放后再唤醒有效后继。卡 · 同步状态 state
- 共享获取结果。
tryAcquireShared返回负数表示失败,返回零表示本次成功但后续获取无法成功,返回正数表示后续仍有成功机会。卡 · 共享获取的返回值 - 线程许可。
LockSupport为每个线程维护至多一个许可,unpark提供许可,park消耗许可或等待。卡 · park 与 unpark 的许可
2. 同步状态 state
同步器需要记录资源是否可用,AQS 用一个 int state 保存这个状态。子类通过下面的方法读取或修改它。
getState() 和 setState() 遵循 volatile 的可见性与顺序约束。compareAndSetState() 则原子地比较并更新状态,避免读取后状态已变却仍按旧值写入。
getState();
setState(next);
compareAndSetState(expected, next);
同一个整数在不同同步器中代表不同含义。可以对照以下三种同步状态理解:
ReentrantLock用它记录当前线程的持有次数。同一线程重复获取这把锁时,计数会加一。CountDownLatch用它记录剩余计数,计数归零后允许等待线程继续执行。Semaphore用它记录可用许可,线程取得许可后才能使用受限资源。
子类通过 tryAcquire、tryRelease 或共享模式的对应方法定义状态转换。AQS 的模板方法则规定获取和等待的整体流程。比如,模板先调用子类尝试获取,失败后再安排线程排队。
3. FIFO 等待队列
资源暂时不可用时,AQS 用 FIFO 队列组织等待线程。FIFO 指先进入的节点排在前面,线程通过队列等待再次尝试获取。
以独占获取为例,可以沿着以下获取流程理解:
- 子类通过
tryAcquire尝试获取状态。 - 获取失败后,线程节点进入等待队列。
- 节点到达合适位置后重新尝试获取。暂时无法继续时,线程通过
LockSupport.park暂停执行。 - 释放方更新状态,并在满足唤醒条件时唤醒有效后继,也就是尚未取消的后续等待者。
- 后继恢复执行后,再次尝试获取状态。
队列按 FIFO 组织等待者,但获取前会先尝试调用子类的方法。这时,新来的线程仍有机会抢先取得资源,所以公平性取决于子类的获取策略。公平实现可用 hasQueuedPredecessors() 检查是否已有线程排在前面,再决定是否获取。
4. 独占模式与共享模式
同步器需要决定资源能由多少线程同时获取。独占模式一次允许一个持有者,比如互斥锁。共享模式则允许多个线程在条件满足时成功获取资源。两种模式的等待者使用同一条同步队列。
共享获取成功后,AQS 会让后继继续检查是否满足获取条件,这称为共享传播。比如,CountDownLatch 归零后,全部等待者都可以继续执行。Semaphore(3) 则只有三个初始许可。每个线程占用一个许可时,最多允许三个线程同时使用资源。后继能否成功,仍取决于剩余许可是否足够。
5. 中断、超时与取消
等待资源时,调用方还要决定是否响应中断,以及最多等待多久。AQS 提供以下三类获取模板:
acquire在等待期间继续获取,即使收到中断也会继续等待。获取成功返回时,它会保留或恢复中断标志。acquireInterruptibly响应中断,退出等待并抛出InterruptedException。tryAcquireNanos同时支持中断和超时,超时仍未取得资源时返回 false。
等待者因中断或超时取消后,队列需要跳过其节点。这样,释放方才能找到仍在等待的有效后继。排队和取消都在并发发生,所以 getQueueLength() 只返回等待线程数的估计值。监控代码应结合多次采样判断积压。
6. Condition 条件队列
等待业务条件与等待锁是两种不同的等待。比如,消费者已经持有锁,却发现队列为空,这时需要等待生产者放入元素。AQS 的 ConditionObject 用独立的条件队列保存这类等待者。
条件等待包含以下队列转移过程:
- 线程调用
await后,节点进入条件队列,并完全释放已持有的同步状态。 - 其他线程持有同一把锁调用
signal,将等待节点转入同步队列。 - 等待线程重新取得同步状态后,才能从等待中返回并继续执行。
因此,signal 发出通知后,等待线程仍要重新竞争锁。子类需要支持独占持有检查,也要能完全释放并恢复原有持有状态。
一把 ReentrantLock 可创建多个 Condition,分别等待“非空”和“未满”,减少无关唤醒。使用方式见 ReentrantLock。
7. AQS 的适用范围
AQS 适合用一个 int 管理同步状态、获取失败后需要排队阻塞的组件。子类也可维护锁所有者等辅助字段,但 AQS 跟踪的同步状态仍是这个整数。自定义获取和释放方法应短小、线程安全,并避免阻塞。
业务代码可以直接使用 JUC,也就是 java.util.concurrent 包提供的并发组件。比如,互斥使用 ReentrantLock,限制并发数使用 Semaphore,等待计数归零使用 CountDownLatch。
自行扩展 AQS 时,需要验证以下协议边界:
- 状态转换是否始终满足资源的使用规则。
- 获取和释放是否正确检查所有权。
- 中断、超时与取消是否让等待者正确退出。
- 序列化后是否能恢复可用状态。AQS 自身只保存同步状态,反序列化后的等待队列为空。
8. 共享获取的返回值
共享获取既要告诉 AQS 本次是否成功,也要说明后续是否还有获取机会。因此,tryAcquireShared(arg) 用以下三个返回区间表达结果:
| 返回值 | 含义 |
|---|---|
| 负数 | 本次获取失败 |
| 零 | 本次成功,后续共享获取无法成功 |
| 正数 | 本次成功,后续共享获取仍有成功机会 |
返回正数表示后继仍有成功机会,具体数值无需等于剩余许可数。后继真正执行时,其他线程也会竞争资源,所以它仍要重新检查状态。释放共享资源时,tryReleaseShared 返回 true 会触发后续唤醒。
9. park 与 unpark 的许可
AQS 通过 LockSupport 暂停和唤醒线程。它为每个线程维护至多一个许可,用来协调 park 与 unpark。
unpark(thread) 为目标线程提供许可,已有许可时仍只保留一个。线程调用 park 时,有许可就消耗它并返回,否则进入等待。因此,目标线程已启动且许可未被消耗时,先 unpark 再 park 也能配对。
park 还会因中断或虚假唤醒返回。虚假唤醒指没有收到对应通知,等待也自行结束的情况。park 返回时会保留中断标志,调用者需要自行检查。因此,等待代码要在循环中重查条件,确认资源可用后再继续。
10. 常见问题
10.1 AQS 与 CLH 队列
AQS 借鉴了 CLH 队列锁组织等待者的方式。经典 CLH 自旋锁由等待者反复读取前驱状态,判断自己能否继续。AQS 则结合 park/unpark 实现阻塞等待,并支持取消、独占和共享传播。因此,分析 AQS 时还需要解释这些等待协议。
10.2 唤醒与锁获取
unpark 只是让等待线程获得继续执行的机会。线程恢复后,需要重新调用子类的获取方法,因为资源仍会被其他线程竞争。只有获取方法成功返回,它才取得了资源。
11 · 面试题卡片
11.1 AQS 如何组织获取失败的线程
出现公司:蚂蚁集团、阅文集团
考察重点
- 说明子类怎样定义
state,以及获取和释放的条件。 - 解释获取失败的线程怎样排队、暂停,并在唤醒后重试。
- 区分独占与共享获取,并说明取消节点为何需要被跳过。
相关内容:同步状态 state至中断、超时与取消。
参考回答
AQS 把获取失败的线程加入 FIFO 等待队列,并安排重试、阻塞和唤醒。
子类先通过 tryAcquire 等方法尝试获取资源。排队后,线程在合适位置重试,暂时无法继续时通过 park 等待。释放方更新状态并唤醒有效后继,后继恢复后再竞争资源。
子类定义 state 的含义和转换条件,AQS 管理独占与共享等待。可中断和超时获取让等待者退出,队列随后跳过取消节点。公平策略则由子类检查前驱等方式实现,因为新线程在入队前也会尝试获取。
11.2 tryAcquireShared 的三个返回区间分别表示什么?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:区分本次获取成功与后继仍可获取,并解释后继为何需要重新检查状态。
相关内容:共享获取的返回值。
参考回答
负数表示本次失败,零表示本次成功但后续无法成功,正数表示本次成功且后续仍有成功机会。
正数只是说明还有获取机会,数值无需等于剩余许可数。后继执行时,资源还会被其他线程竞争,所以它必须重新检查状态。
11.3 先调用 unpark 再调用 park 会丢失唤醒吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释许可为何最多积累一个,以及中断和虚假唤醒为何要求循环检查条件。
相关内容:park 与 unpark 的许可。
参考回答
目标线程已启动且许可未被消耗时,先调用 unpark 会保留许可。
下一次 park 消耗这个许可后直接返回,因此这次唤醒可以先于等待发生。
连续两次 unpark 仍只积累一个许可。此外,park 会因中断或虚假唤醒返回,所以返回后仍需循环检查资源条件。