AQS 的核心"可扩展点"看起来很多,但底层抓住一个东西就够了:
- AQS 用一个
volatile int state表示同步状态 ,所有的"能不能进临界区"最终都落在对state的 CAS 更新上。
你必须记住的 3 句话(面试直出):
volatile state让线程看到"最新同步状态",避免读到旧值。- CAS 让"从旧状态到新状态"的竞争具备原子性,决定谁能成功。
- AQS 在 CAS 失败后用队列 +
park/unpark控制成本,避免所有线程无序自旋。
1. state 不是"锁状态",是"同步器状态"
state 的含义由同步器自定义:
ReentrantLock:state常用来表示重入次数(0=未锁,>0=已锁且次数)Semaphore:state表示许可数量CountDownLatch:state表示计数器(到 0 才放行)
面试表达:
- AQS 不关心业务语义,它只提供 state 的原子修改与排队阻塞模板。
2. 为什么 state 必须是 volatile
volatile 解决两件事:
- 可见性:一个线程 release 后,其他线程能看到 state 的变化
- 有序性(happens-before):确保 release 前的写,在 acquire 后可见(配合具体实现与内存语义)
面试时更"工程化"的说法:
- 线程 A 在临界区里写的数据,必须在它 unlock 之后对线程 B 可见;否则就算"拿到了锁",读到的也可能是旧数据。
你可以把它理解成:
- 没有 volatile,线程可能一直读到旧值,出现"锁释放了但别人看不见"。
3. 为什么仅有 volatile 不够,还要 CAS
volatile 只能保证读写可见、有序,但不保证复合操作原子性。
例如独占锁获取时:
- 你需要把
state从 0 改成 1 - 如果两个线程同时看到 0,再同时写 1,必须只有一个成功
所以必须使用 CAS:
compareAndSetState(expect, update)
其本质:
- CPU 原子指令支持的"比较并交换",成功者获得资格
补充一个常见追问:CAS 失败了会怎样?
- 失败说明有人更早更新了 state(你看到的期望值已经过期)
- AQS 的做法不是"永远死循环 CAS",而是:
- 先走快路径抢一次
- 失败再入队
- 入队后在合适时机再竞争,仍失败就 park
4. 一个最小可运行的"自定义独占锁"骨架
下面是 AQS 的典型使用姿势(用于理解,不追求功能完整):
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class SimpleMutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 只允许从 0 -> 1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
@Override public void lock() { sync.acquire(1); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public Condition newCondition() { return sync.newCondition(); }
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
}
关键点:
- 原子性来自 CAS :
compareAndSetState(0, 1) - 排队/阻塞来自 AQS :
acquire(1)内部处理入队、park - 释放唤醒来自 AQS :
release(1)会唤醒后继节点
你在面试里可以补一句"owner 的意义":
setExclusiveOwnerThread不是为了同步(同步靠 state + 队列),而是为了提供"当前谁持有独占资源"的语义支持(例如可重入、校验是否非法释放)。
5. 常见误区
-
误区 1:volatile 就是原子
- 错。volatile 不能保证
state++这种复合操作原子。
- 错。volatile 不能保证
-
误区 2:CAS 一定比 synchronized 快
- 错。竞争激烈/临界区长时,自旋重试会让 CPU 飙升。
-
误区 3:state 只能 0/1
- 错。ReentrantLock 用 state 表示重入次数;Semaphore 用 state 表示许可。
-
误区 4:CAS 失败就一直自旋直到成功
- 错。AQS 的核心价值之一就是:让失败者进队列、阻塞等待,避免所有线程一起空转。
6. 口述检查点(你要能讲清楚)
-
Q:
state为什么是 int,不是 long?- A:多数同步器用 int 足够;更重要是 AQS 设计目标是轻量、通用,扩展语义由上层决定。
-
Q:
compareAndSetState是怎么实现的?- A:底层是
Unsafe/VarHandle调用 CPU CAS 指令,保证原子更新。
- A:底层是
-
Q:为什么 acquire 要先 CAS,失败才入队?
- A:减少不必要的阻塞;多数情况下锁可能很快释放,先快路径抢一次更高效。
-
Q:
state的修改和临界区内的读写之间,怎么建立可见性关系?- A:unlock 过程中对 state 的写与唤醒语义,配合 volatile 的内存语义,使得后续成功 acquire 的线程能看到之前临界区内的写入结果(在 JMM 下形成同步关系)。
7. 30 秒背诵稿
AQS 用一个 volatile int state 表示同步状态,volatile 保证可见性与一定的有序性;但"从旧状态更新到新状态"的竞争必须靠 CAS 才能保证原子性,所以获取锁本质是 CAS 改 state,成功就进入临界区,失败就进入 AQS 队列并 park 阻塞;释放时把 state 复位并唤醒队列后继。