AQS 的原理是什么?
简化版
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 JUC 里一堆锁和同步工具的共同基类/框架。它用一个 volatile int state(同步状态)+ CAS + 一个 FIFO 双向队列(存放抢锁失败被阻塞的线程)搭起了「加锁—阻塞排队—唤醒」的通用骨架。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 全都基于它。
详细版
AQS 的三大核心:
- state(同步状态):一个
volatile int,用它表示「锁/资源的状态」。不同工具赋予它不同含义:ReentrantLock:state = 加锁次数(0 未锁,>0 已锁且记录重入次数);Semaphore:state = 剩余许可数;CountDownLatch:state = 还需 countDown 的次数。
- CAS 改 state:多线程用 CAS 竞争地修改 state,保证原子性,这是「无锁抢锁」的关键。
- CLH 变体的 FIFO 双向队列:抢 state 失败的线程,被包成 Node 加入队列并阻塞(
LockSupport.park);持锁线程释放时,唤醒队列头部的线程来抢。
模板方法设计:AQS 把「排队、阻塞、唤醒」这些通用逻辑写好,只留几个方法给子类实现——子类只需定义「怎么算获取成功/释放成功」(即怎么改 state),如 tryAcquire/tryRelease。
完整版教学
一、AQS 解决的问题:把「锁的通用部分」抽出来
写一个锁,其实大部分逻辑是重复的:抢不到锁怎么把线程挂起、锁释放了怎么唤醒下一个、怎么维护等待队列、怎么处理公平/非公平……这些和「锁本身的语义」无关,是所有同步器共用的骨架。
AQS 就是把这套骨架抽出来做成模板:队列管理、阻塞唤醒、状态的 CAS 竞争都由 AQS 实现好,具体的锁只要回答一个问题——「state 达到什么条件算获取成功/释放成功」。这就是为什么 ReentrantLock、Semaphore、CountDownLatch 代码量都很小,因为繁重的部分 AQS 全包了。
二、加锁流程:state + 队列怎么配合
以 ReentrantLock(非公平)加锁为例:
- 线程用 CAS 把 state 从 0 改成 1——成功就拿到锁,把自己设为持有者(
exclusiveOwnerThread); - 失败(锁被占)→ 若持有者是自己,state + 1(可重入);否则进入下一步;
- 把线程包成 Node 加入 FIFO 队列尾部,然后
LockSupport.park()阻塞自己,等待被唤醒; - 持锁线程
unlock时,state 减到 0,唤醒队列头部的后继节点,被唤醒的线程再去抢 state。
整个过程:state 表示资源、CAS 抢资源、抢不到就进队列阻塞、释放时唤醒队首。
三、独占 vs 共享两种模式
AQS 支持两种资源获取模式:
- 独占(Exclusive):同一时刻只有一个线程能拿到,如
ReentrantLock。state 就是 0/1(+重入)。 - 共享(Shared):多个线程可同时拿到,如
Semaphore(多个许可)、CountDownLatch(计数到 0 全部放行)、读写锁的读锁。
同一个队列、同一套阻塞唤醒机制,靠子类实现 tryAcquire(独占)或 tryAcquireShared(共享)来区分,非常灵活。
四、公平锁 vs 非公平锁
AQS 也统一支撑了公平/非公平:
- 非公平锁(默认):新来的线程先直接 CAS 抢一把 state,抢到就插队成功,抢不到才乖乖排队。吞吐高(减少了线程切换),但可能让排队的线程「饿着」。
- 公平锁:新线程先检查队列里有没有人在等,有就老实排到队尾,严格 FIFO。公平但吞吐低。
new ReentrantLock(true) 就是公平锁。区别只在「抢锁前要不要先看队列」这一点,其余队列/阻塞逻辑复用 AQS。
五、基于 AQS 的常见工具
| 工具 | state 的含义 | 模式 |
|---|---|---|
| ReentrantLock | 加锁次数(重入) | 独占 |
| Semaphore | 剩余许可数 | 共享 |
| CountDownLatch | 剩余计数 | 共享 |
| ReentrantReadWriteLock | 高 16 位读锁、低 16 位写锁 | 读共享/写独占 |
理解了 AQS,这些工具就都是「给 state 赋予不同含义」的变体,一通百通。
六、Condition 为什么还有单独等待队列
AQS 的同步队列解决“谁在等获取同步状态”,ConditionObject 则解决“已经拿到锁的线程因业务条件不满足而等待”。调用 await() 时,线程会释放当前独占状态并进入该 Condition 的条件队列;其他线程调用 signal() 只会把条件节点转移到同步队列,线程仍要重新竞争锁才能从 await() 返回。
Condition 条件队列 --signal--> AQS 同步队列 --重新获锁--> 继续执行
一个 ReentrantLock 可以创建多个 Condition,把“队列非空”“队列未满”等不同等待集合分开。signal() 不是把锁直接交给目标线程,也不保证目标线程立刻运行;这些边界正是面试常追问点。
七、用一个许可数例子理解共享模式
假设 Semaphore 初始有 3 个许可,A、B、C 各成功获取 1 个后,AQS state 从 3 依次变为 2、1、0。线程 D 再尝试获取时失败并进入同步队列;B 释放 1 个许可后 state 变回 1,传播唤醒使等待者有机会继续获取。
| 时刻 | 操作 | state | 结果 |
|---|---|---|---|
| T1 | A acquire | 2 | 成功 |
| T2 | B、C acquire | 0 | 都成功 |
| T3 | D acquire | 0 | 入队等待 |
| T4 | B release | 1 | 唤醒/传播 |
独占模式通常让一个成功节点拥有状态,共享模式则允许剩余资源继续向后传播。state 的业务含义由子类定义,因此它既能表示重入次数,也能表示许可数或剩余计数。
记忆钩子:AQS 的主线是“CAS 改 state,失败进同步队列,前驱条件满足后再尝试”;Condition 只是先在条件队列等业务信号,收到信号后仍要回同步队列抢锁。
八、常见误区与追问
- 误区:AQS 本身就是一把可直接使用的锁。 它是同步器框架,
ReentrantLock、Semaphore等在其上定义 state 语义和获取释放规则。 - 误区:AQS 的 state 只能取 0 或 1。 它是
int同步状态,可表示重入次数、许可数或倒计数,具体由实现解释。 - 误区:非公平锁完全不使用队列。 抢占失败后仍会排队,只是新线程可能先做一次直接 CAS。
- 追问:AQS 为什么用 CAS 修改 state? 获取与释放会被多线程并发执行,CAS 可在不额外加内部互斥锁的情况下原子更新状态。
- 追问:被
signal()的线程会立即执行吗? 不会;它先转移到同步队列,还要重新获得关联锁。 - 追问:取消或中断的节点怎么办? AQS 会标记并跳过、清理取消节点,具体链接修复属于实现细节,不能把队列理解成永不变化的普通链表。
- 追问:公平锁为什么吞吐常低一些? 它更严格检查前驱,减少插队却增加调度、上下文切换和缓存失效机会。
九、加强记忆
把 AQS 串成“状态、队列、阻塞、唤醒”四件事:子类用 CAS 定义 state 的获取释放,失败线程包装为节点进入同步队列,再通过 LockSupport.park/unpark 配合前驱状态管理等待。独占与共享的差别在于成功后是否还能向后传播资源,公平与非公平的差别在于新线程能否先尝试抢占。Condition 另有条件队列,但信号只负责转移节点,最终仍要回同步队列重新获锁。面试回答应先讲框架骨架,再用 ReentrantLock 或 Semaphore 的具体 state 数值把流程落地。