07 · AQS 与 Lock 实现原理(含 CAS)
对应学习计划:第一月第2周「Java 并发编程(重中之重!薪资分水岭)」
面试定位:AQS 是 JUC 的「地基工程」,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 全部建立在它之上。懂 AQS = 看懂了 JUC 半边天。
一、本文能帮你回答的面试题
- 什么是 AQS?它的核心思想是什么?
- AQS 的数据结构(state + CLH 队列)?为什么用 CLH 变体队列?
- ReentrantLock 源码:lock/unlock 流程?
- 公平锁和非公平锁的区别?(为什么不公平锁性能反而好)
- ReentrantLock 和 synchronized 的区别?
- CAS 是什么?有什么缺陷?怎么解决 ABA?
- Semaphore / CountDownLatch / CyclicBarrier 各自什么场景?
二、概念介绍
AQS(AbstractQueuedSynchronizer,抽象队列同步器):JUC 锁的基础框架。核心思想:
用「一个状态变量 state + 一个 FIFO 等待队列」管理所有线程的获取/释放资源的同步逻辑。
java
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
// 内部类:CLH 变体双向队列(把抢不到锁的线程排成队)
static final class Node {
volatile Node prev, next;
volatile Thread thread; // 队里等着谁
volatile int waitStatus; // 节点状态(-1=SIGNAL 要唤醒下一个)
}
private transient volatile Node head, tail; // 队首队尾
}
AQS 提供两种模式:
- 独占模式(EXCLUSIVE):一把锁只有一个线程持有,如 ReentrantLock。
- 共享模式(SHARED):多个线程可以同时持有,如 Semaphore、CountDownLatch。
继承体系(面试组织答案的骨架):
Lock 接口
└── ReentrantLock ── 内部 Sync extends AQS(FairSync/NonfairSync)
ReentrantReadWriteLock.ReadLock / WriteLock ── AQS
Semaphore(共享计数)─ AQS
CountDownLatch ── AQS(共享)
CyclicBarrier ── 基于 ReentrantLock + Condition(不是直接 AQS)
三、原理深入
3.1 CLH 队列:为什么无法抢到锁的线程要进队列阻塞
- 直接自旋空转浪费 CPU;直接进 OS 挂起又太贵。
- CLH 变体队列:刚到的线程先自旋几次(很短),不行就把自己包装成 Node 挂到队尾,然后 park(挂起)。
- 前驱节点释放时,会通过
unpark唤醒后继节点------避免惊群(只唤醒一个)。
3.2 CAS(Compare And Swap)------AQS 和 JUC 的原子基石
java
// 语义:比较-交换,硬件级原子指令(CPU 保证)
// 当前值 == 期望值 ? 改成新值,返回 true : 返回 false,改不了
boolean compareAndSet(int expect, int update);
// 底层:Unsafe.compareAndSwapInt(object, offset, expect, update)
CAS 三个参数 :内存位置、期望值、新值。核心价值:不加锁实现并发安全(乐观锁思想)。
CAS 缺陷(必考):
- ABA 问题 :变量 A 被改成 B 又改回 A,CAS 发现值没变就成功了,但中间被改过。解决:
AtomicStampedReference(带版本号)。 - 自旋开销:竞争激烈时 CPU 空转。
- 只能保证单个变量原子 :多个变量需要锁或包装成对象(
AtomicReference)。
3.3 ReentrantLock 核心流程
acquire(1) (以非公平锁为例)
├─ CAS 尝试 state 0→1
│ 成功 → 当前线程持锁
│ 失败 ↓
├─ 判断是否可重入(当前线程 == exclusiveOwnerThread?)
│ 是 → state += 1(重入计数)
│ 否则 ↓
└─ 加入 CLH 队尾(Tail CAS 入队)→ 自旋/park 等待
前驱节点释放后把它唤醒 → 醒来后再次 tryAcquire
release(1)
└─ state -= 1
若 state == 0 → 真正释放独占 → 唤醒队头后继节点
非公平 vs 公平:
java
// 非公平:线程直接抢一次(插队),抢不到才排队
final boolean nonfairTryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) { ... } // 先插队
...
}
// 公平:先看队列有没有人,有就不抢
protected final boolean tryAcquire(int acquires) {
if (hasQueuedPredecessors()) return false; // 队列非空就让位
...
}
为什么非公平锁更快:新线程「插队成功」就不用做线程上下文切换(park + unpark 很贵);公平锁强制排队,队尾总是被唤醒-挂起-再唤醒,切换成本高。所以 JDK 的 ReentrantLock 默认非公平,吞吐更大(代价是「饿死」风险,但统计上可接受)。
3.4 Condition:精确唤醒(synchronized wait/notify 的替代)
java
// Condition 绑定在 Lock 上,每个 Condition 一条独立等待队列
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产:lock.lock(); while (队列满) notFull.await(); 入队; notEmpty.signal(); unlock();
// 消费:lock.lock(); while (队列空) notEmpty.await(); 出队; notFull.signal(); unlock();
对比 old synchronized:只能 wait/notifyAll 全部唤醒;Condition 可以定向唤醒(唤醒生产者 或 消费者),减少无谓唤醒。
3.5 ReentrantLock vs synchronized(终极对比表)
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 使用 | 关键字,无感 | API,必须手动 unlock(finally) |
| 锁粒度 | 方法/代码块 | 更灵活(tryLock、lockInterruptibly) |
| 可否限时获取 | ❌ | ✅ tryLock(1, TimeUnit.SECONDS) |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 公平策略 | 非公平 | 可指定公平/非公平 |
| 条件队列 | 1 个 | 多个 Condition 精确唤醒 |
| 性能 | JDK6 后大幅优化,两者相当 | 相当 |
| 底层 | Monitor(ObjectMonitor) | AQS(CLH + LockSupport) |
给面试的结论:JDK6 优化后 synchronized 性能已追平 ReentrantLock;优先用 synchronized(简单、不易错),只有需要「限时获取、可中断、多条件、公平」时才用 ReentrantLock。
四、实战例子
实战 1:经典并发场景对比------用 ReentrantLock 实现限时抢锁
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private static final ReentrantLock LOCK = new ReentrantLock();
public static void main(String[] args) {
// 线程1 长期持有
new Thread(() -> {
LOCK.lock();
try { Thread.sleep(5000); } catch (Exception e) {}
finally { LOCK.unlock(); }
}, "占用者").start();
// 线程2:最多等2秒,抢不到就放弃(synchronized 做不到!)
Runnable task = () -> {
boolean ok = false;
try { ok = LOCK.tryLock(2, TimeUnit.SECONDS); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println(Thread.currentThread().getName() + " 抢锁结果: " + ok);
if (ok) LOCK.unlock();
};
new Thread(task, "面试者A").start();
new Thread(task, "面试者B").start();
}
}
实战 2:手写一个「先进先出队列」供生产者-消费者使用(Condition 实战)
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BlockingQueueDemo<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列不满
private final Condition notEmpty = lock.newCondition(); // 队列不空
public BlockingQueueDemo(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) notFull.await(); // 满了等
queue.add(item);
notEmpty.signal(); // 精准唤醒消费者
} finally { lock.unlock(); }
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await(); // 空了等
T item = queue.poll();
notFull.signal(); // 精准唤醒生产者
return item;
} finally { lock.unlock(); }
}
}
实战 3:ABA 问题的演示与解决
java
import java.util.concurrent.atomic.*;
public class AbaDemo {
public static void main(String[] args) throws InterruptedException {
// 有 ABA:值 A -> B -> A,CAS 误判「没被改过」
AtomicInteger num = new AtomicInteger(10);
// 用带版本的解决:AtomicStampedReference
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);
int stamp = ref.getStamp();
// 模拟:线程A做 CAS(10 -> 20),期间有人把值改了又改回来
ref.compareAndSet(10, 12, stamp, stamp + 1); // 版本号也变了,能感知
System.out.println("当前值: " + ref.getReference() + ", 版本: " + ref.getStamp());
}
}
实战 4:Semaphore(信号量)------控制并发访问数
java
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
// 只允许 3 个线程同时访问(如数据库连接池只放行3个)
private static final Semaphore SEM = new Semaphore(3);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
SEM.acquire();
System.out.println(Thread.currentThread().getName() + " 进入,剩余许可: " + SEM.availablePermits());
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
SEM.release();
}
}, "线程" + i).start();
}
}
}
五、面试话术(背下来)
Q:AQS 是什么?
「AQS 是 JUC 锁的统一框架,核心是一个 volatile 的 int state + CLH 变体双向等待队列。获取资源成功就改 state;失败就把线程包装成 Node 挂到队列里 park 挂起;释放时唤醒后继。独占/共享两种模式,ReentrantLock/Semaphore/CountDownLatch 都是基于它,只需要子类实现 tryAcquire/tryRelease 决定什么时候算获取成功。」
Q:ReentrantLock 的原理?
「内部 Sync 继承 AQS。lock()=acquire(1):先尝试 CAS state 0→1,成功持锁;失败则看是不是当前线程重入(state+1);不是就 CAS 入队等待。unlock()=release(1):state-1,减到 0 真正释放并唤醒队头。支持重入靠 exclusiveOwnerThread 记录持锁线程。」
Q:公平锁和非公平锁?
「非公平锁是线程进来直接先 CAS 试抢一次,抢不到才排队------优点是减少了大量 park/unpark 上下文切换,吞吐高,所以默认非公平;缺点是可能造成队尾线程饿死。公平锁先查 hasQueuedPredecessors,队列有人就让位。」
Q:CAS 缺点?
「三个:ABA 问题(用 AtomicStampedReference 加版本号解决);自旋消耗 CPU(竞争激烈时不划算);只能原子操作单个变量(多个字段一致性要用锁或 AtomicReference 包结构体)。」
Q:什么时候用 ReentrantLock?
「需要 tryLock 限时抢锁、lockInterruptibly 可中断、多个 Condition 精确唤醒、公平锁时用它;简单互斥场景 synchronized 更简洁,JDK6 后性能也差不多。」
六、踩坑总结(易错点)
| # | 坑 | 说明 |
|---|---|---|
| 1 | 忘记在 finally 里 unlock | 抛异常后锁永不释放,直接死锁 |
| 2 | lock 顺序不一致导致死锁 | 多把锁要按固定顺序获取 |
| 3 | lock() 后还 tryLock |
语义混淆;tryLock 失败不算「获得锁」,不能在 finally 无脑 unlock |
| 4 | Condition 的 await 忘 while | 和 wait 一样要 while 防虚假唤醒 |
| 5 | 把 Semaphore 当「有界队列」 | 它只限并发量,资源释放要靠 acquire/release 配对,漏 release 会泄漏许可 |
| 6 | CountDownLatch 不可复用 | 一次性;要复用用 CyclicBarrier |
| 7 | 没考虑中断 | lockInterruptibly/tryLock 抛 InterruptedException 必须恢复中断位或处理 |
七、课后作业
- 手写 ReentrantLock 的核心(state + 重入 + CLH 简易队列),验证互斥。
- 用 CountDownLatch 并行查询「4 个上游接口」,全部返回后聚合结果(配合 CompletableFuture 更优雅,见第10篇)。
- 思考:为什么 AQS 用双向队列而不是单向队列(提示:需要支持中途取消节点/查找前驱状态)。
- 对比 CyclicBarrier 与 CountDownLatch,各举一个业务场景。
下一篇预告:线程池七大参数与四种拒绝策略。