Lesson 16 · 并发编程

AQS 框架与 ReentrantLock:独占锁、公平锁、Condition

深度·🔥 极高·#并发·#核心·#

第 1 站

面试官:你知道 Java 里的锁底层是怎么实现的吗?

"synchronized 你说了 JVM 层面的 Monitor,那 ReentrantLock 呢?CountDownLatch、Semaphore 底层又是什么?它们之间有没有共通的东西?" —— 面试官想考察的不是某个 API 用法,而是你对 java.util.concurrent 底层框架的理解深度。

java.util.concurrent(JUC)包中,几乎所有同步工具背后都站着同一个"幕后大佬"——AQS(AbstractQueuedSynchronizer)。这个由并发大师 Doug Lea 设计的抽象框架,是整个 JUC 锁体系的基石:

  • ReentrantLock —— 可重入的独占锁,AQS 最经典的实现
  • CountDownLatch —— 倒计数器,state 从 N 递减到 0 时释放所有等待线程
  • Semaphore —— 信号量,state 表示可用许可数
  • ReentrantReadWriteLock —— 读写锁,state 高 16 位存读锁计数、低 16 位存写锁计数
  • ThreadPoolExecutor 的 Worker —— 内部也通过 AQS 实现不可重入锁

理解 AQS = 理解 JUC 的一半。核心思想极其精炼:

AQS 核心公式
一个 volatile int state(同步状态) + 一条 FIFO 双向队列(CLH 变体) + 模板方法模式(子类重写 tryAcquire / tryRelease)

本文以 ReentrantLock 为主线,从 AQS 数据结构讲起,逐行拆解 lock()、unlock()、公平/非公平、Condition 全流程,最后横向串联其他 AQS 工具。

第 2 站

AQS 核心结构:state + CLH 队列

AQS 的全部魔力建立在两个核心成员之上。先看源码:

AbstractQueuedSynchronizer.java · JDK 8
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
    // 同步状态:ReentrantLock 中 0=空闲,>0=重入次数;Semaphore 中=许可数
    private volatile int state;
    // CLH 变体队列的头尾节点
    private transient volatile Node head;
    private transient volatile Node tail;
    // 独占模式:子类重写
    protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
    protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
    // 共享模式:CountDownLatch / Semaphore 重写
    protected int tryAcquireShared(int arg) { ... }
    protected boolean tryReleaseShared(int arg) { ... }
}

Node 是 CLH 队列的节点单元,包含五个关键字段:

Node.java · AQS 内部静态类
static final class Node {
    static final int CANCELLED = 1;    // 已取消(超时/中断)
    static final int SIGNAL    = -1;   // 后继需要被 unpark
    static final int CONDITION = -2;   // 在 Condition 队列中
    static final int PROPAGATE = -3;   // 共享模式传播唤醒
    volatile int waitStatus;
    volatile Node prev;       // 前驱(取消时回溯找有效节点)
    volatile Node next;       // 后继(unpark 目标)
    volatile Thread thread;
    Node nextWaiter;          // Condition 后继 / 独占or共享标识
}
AQS 核心结构:volatile state + CLH 变体队列 volatile int state CAS 操作,保证可见性 head (dummy) waitStatus = 0 Node A ws = SIGNAL(-1) Thread-1 Node B ws = SIGNAL(-1) Thread-2 Node C ws = 0 Thread-3 next → ← prev(取消时回溯) head tail AQS 三板斧 ① state 通过 CAS 原子修改 ② 获取失败的线程入 CLH 队列 park 阻塞 ③ 子类重写 tryAcquire/tryRelease 实现定制同步语义
图 1AQS 核心结构:volatile state 管理同步状态,CLH 变体队列管理等待线程
为什么 state 必须是 volatile?

state 的修改通过 compareAndSetState() CAS 操作保证原子性,但 volatile 保证了可见性:当一个线程 CAS 修改 state 后,其他线程通过普通读(getState())能立刻看到最新值,而不需要额外加锁。没有 volatile,线程可能读到 CPU 缓存中的旧值。

第 3 站

CLH 队列:从自旋到 park 的进化

原始 CLH(Craig, Landin, Hagersten)是一种自旋锁队列:每个节点只包含一个 locked 字段,线程通过自旋(while(node.prev.locked))检查前驱状态。自旋的优点是响应快(无需内核态切换),但在锁竞争激烈的场景下,大量线程持续空转,CPU 利用率极高但有效吞吐为零

AQS 做了三个关键改造,从根本上解决了自旋锁的缺陷:

  • 变单向为双向:增加 prev 指针,用于取消节点时向前回溯跳过已取消的前驱
  • 自旋改 park/unpark:不再忙等,调用 LockSupport.park() 将线程挂起(OS 层面进入 BLOCKED 状态),unpark() 唤醒(恢复到 RUNNABLE)。park/unpark 底层依赖操作系统的 Mutex/Condition 或 Linux 的 futex
  • 增加 waitStatus:节点携带 SIGNAL、CANCELLED、CONDITION、PROPAGATE 等精细状态,精确控制唤醒时机
AQS 的 CLH 变体队列(双向 + park/unpark) head 虚拟哨兵 Node SIGNAL(-1) Thread-A (parked) Node SIGNAL(-1) Thread-B (parked) Node (tail) ws = 0 Thread-C (parked) next →(用于 unpark 后继) ← prev(取消时回溯跳过无效节点) 入队流程:addWaiter + enq ① 创建 Node ② CAS 将 tail 指向自己(失败重试)③ node.prev = oldTail, oldTail.next = node enq 使用 for(;;) 无限重试 CAS,保证在并发下一定能入队成功
图 2CLH 变体队列:双向链表 + park/unpark,head 是虚拟哨兵节点
为什么需要 prev 指针?

当线程被中断或超时取消时,需要从队列中"摘除"。如果只有 next,无法找到前驱修补链路。有了 prev,取消的节点沿 prev 向前找到第一个非取消前驱,把自己的 prev 指过去,同时更新前驱的 next,完成摘除。这就是 cancelAcquire() 的核心逻辑。

第 4 站

ReentrantLock.lock() 全流程拆解(NonfairSync)

ReentrantLock 默认是非公平锁(NonfairSync)。调用 lock() 时的完整流程:

ReentrantLock.NonfairSync · lock()
final void lock() {
    // ① 上来先 CAS 抢锁,不管队列有没有人 —— "插队"尝试
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // ② CAS 失败,走 AQS 标准流程
}

acquire(1) 是 AQS 的模板方法,依次调用子类 tryAcquire()、失败则入队 park:

AQS.acquire() · 模板方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();  // 等待中被中断过,补上中断标记
}

NonfairSync 的 tryAcquire() 委托给 Sync.nonfairTryAcquire():

ReentrantLock.Sync · nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // ③ state==0 锁空闲,CAS 抢占
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        // ④ 当前线程已持有 → 重入,state+1
        int nextc = c + acquires;
        setState(nextc);  // 无需 CAS(只有自己能改)
        return true;
    }
    return false;  // ⑤ 被其他线程持有,获取失败
}

tryAcquire 失败后,AQS 执行 addWaiter() 入队,然后进入 acquireQueued() 自旋+park 循环:

AQS.acquireQueued() · 核心循环
final boolean acquireQueued(Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 前驱是 head 才有资格尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);  // 成功,自己成为新 head
                p.next = null;  // 旧 head 断开,帮助 GC
                failed = false;
                return interrupted;
            }
            // 获取失败:判断是否应 park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed) cancelAcquire(node);
    }
}
shouldParkAfterFailedAcquire 做了什么?

检查前驱的 waitStatus。如果是 SIGNAL(-1),说明"我释放时会通知你",可以放心 park。如果前驱已取消(>0),沿 prev 跳过所有取消节点重新链接。如果前驱既不是 SIGNAL 也不是 CANCELLED,则 CAS 设为 SIGNAL,但不立即 park,再自旋一次尝试获取锁。

lock() 关键路径

lock() → CAS(0→1) 成功则直接获得锁;失败 → acquire(1)tryAcquire(CAS + 重入检查)→ 失败 → addWaiter 入队 → acquireQueued 自旋+park → 被 unpark 后重试 → 成功则将自身设为 head。

除了 lock(),ReentrantLock 还提供两个重要变体:lockInterruptibly() 在等待过程中响应中断(内部调用 acquireInterruptibly,park 后检查中断状态并抛出 InterruptedException);tryLock(timeout, unit) 支持超时等待(内部调用 tryAcquireNanos,计算 deadline,每次 park 后检查是否超时)。这两个能力是 synchronized 不具备的,也是 ReentrantLock 在实际开发中的核心优势之一。

第 5 站

公平锁 vs 非公平锁:一行代码的差异

通过构造函数选择模式,默认非公平:

ReentrantLock · 构造函数 + FairSync.tryAcquire()
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

// FairSync 的 tryAcquire —— 比 NonfairSync 多了一个检查
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // ★ 关键差异:先检查队列中是否有人排队
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);  // 重入
        return true;
    }
    return false;
}

// AQS 提供的判断方法
public final boolean hasQueuedPredecessors() {
    Node h = head, t = tail, s;
    // 队列非空 且 我不是第一个等待者 → 有人排在我前面
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平锁的 lock() 第一步就是无条件 CAS——不管队列里有没有人,上来就抢。这就是"非公平"的含义:刚来的线程可以"插队"到已排队线程前面

维度非公平锁(NonfairSync)公平锁(FairSync)
lock() 第一步无条件 CAS(0→1),直接抢锁先 hasQueuedPredecessors(),没人排队才 CAS
吞吐量高(减少 park/unpark 上下文切换)较低(严格按序,即使锁刚释放也排队)
饥饿风险存在(某些线程可能长期抢不到)不存在(FIFO 保证公平)
适用场景绝大多数场景(默认选择)资源分配需严格公平(订单处理、任务调度)
面试官追问:"为什么非公平锁性能更好?"

核心原因是减少线程上下文切换。假设线程 A 刚释放锁,线程 B 正好来请求:

  • 非公平:B 直接 CAS(0→1) 成功,立刻执行。省去 park → unpark → 调度 → 恢复的全部开销
  • 公平:B 发现队列中有线程 C,必须入队。最终 C 被 unpark → 调度 → 获锁 → 释放 → 再 unpark B,多一次完整上下文切换
面试回答要点

"公平和非公平的区别在于 tryAcquire 中是否调用 hasQueuedPredecessors()。非公平允许刚到的线程直接 CAS 抢锁,减少 park/unpark 开销,吞吐量更高;缺点是可能饥饿。公平严格按 FIFO,保证不饥饿但性能较低。绝大多数场景用非公平即可。"

第 6 站

unlock() 流程:释放锁与唤醒后继

unlock() 委托给 AQS 的 release() 模板方法:

ReentrantLock + AQS · unlock 全流程
public void unlock() { sync.release(1); }

// AQS.release() 模板方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 唤醒后继
        return true;
    }
    return false;
}

// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = (c == 0);
    if (free) setExclusiveOwnerThread(null);
    setState(c);  // 无需 CAS:只有持有者会修改 state
    return free;
}

unparkSuccessor()tail 往前找第一个非取消节点并唤醒:

AQS.unparkSuccessor()
private void unparkSuccessor(Node node) {
    if (node.waitStatus < 0)
        compareAndSetWaitStatus(node, node.waitStatus, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0) s = t;
    }
    if (s != null) LockSupport.unpark(s.thread);
}
为什么从 tail 往前找,不直接取 head.next?

入队的 enq() 分两步:① node.prev = tail(CAS),② oldTail.next = node。两步间有时间窗口——tail 已指向新节点,但旧 tail 的 next 还没指过来。从 head.next 往后遍历可能在中间断裂。而从 tail 沿 prev 往前找是安全的,因为 prev 在 CAS 之前就已赋值。

unlock() 关键路径

unlock()release(1)tryRelease:state-1 → 若 state==0 清除 owner → unparkSuccessor:从 tail 往前找有效节点 → LockSupport.unpark() → 被唤醒线程在 acquireQueued 循环中重新竞争锁。

第 7 站

Condition 条件队列:两条队列的协作

newCondition() 创建的 ConditionObject 是 AQS 内部类,维护一条独立于 sync 队列的条件等待队列——AQS 最精巧的设计之一。

ConditionObject · await() / signal()
public final void await() throws InterruptedException {
    Node node = addConditionWaiter();      // ① 加入条件队列
    int savedState = fullyRelease(node);   // ② 完全释放锁(保存重入次数)
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);            // ③ park 阻塞
    }
    acquireQueued(node, savedState);        // ④ 回到 sync 队列重新竞争锁
}
public final void signal() {
    Node first = firstWaiter;
    if (first != null) doSignal(first);    // 唤醒条件队列第一个节点
}
private void doSignal(Node first) {
    do {
        if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&   // 转移到 sync 队列
             (first = firstWaiter) != null);
}
Condition 机制:sync 队列与条件队列的协作 AQS Sync Queue(同步队列) head dummy Thread-X ws=SIGNAL Thread-Y ws=SIGNAL signal 后转入 重新竞争锁 Condition Queue(条件队列)—— 单向链表,nextWaiter 链接 Thread-A CONDITION(-2) Thread-B CONDITION(-2) Thread-C CONDITION(-2) firstWaiter lastWaiter signal():转移节点到 sync 队列并 unpark await() / signal() 完整流程 await():释放锁 → 入条件队列 → park → 被 signal 后转入 sync 队列 → 重新竞争 signal():取条件队列 firstWaiter → 转入 sync 队列 → unpark 该线程
图 3Condition 双队列协作:await 将线程从 sync 队列移入条件队列,signal 将其转回
await() vs Object.wait()

两者都需要持锁调用、都释放锁并阻塞。关键差异:Object.wait() 只有一个 waitSet,Condition 可以创建多个实现精确唤醒(如 ArrayBlockingQueue 的 notFull/notEmpty);Condition 支持超时、不可中断等变体;底层机制不同——wait 基于 monitor,Condition 基于 AQS park/unpark。

Condition 面试回答

"Condition 是 AQS 内部类 ConditionObject,维护独立条件队列。await() 释放锁、入条件队列、park;signal() 将头部节点转到 sync 队列、unpark。被唤醒后在 sync 队列重新竞争锁。多个 Condition 可实现精确唤醒,如 BlockingQueue 中的 notFull 和 notEmpty。"

除了 signal() 只唤醒一个节点,signalAll() 会将条件队列中所有节点转移到 sync 队列并逐一 unpark。典型场景如生产者-消费者模式中,生产者生产一个产品后调用 signal()(只需唤醒一个消费者),而条件变化影响所有等待者时用 signalAll()

维度Object.wait()Condition.await()
前置条件持有 synchronized 监视器锁持有 ReentrantLock
等待队列数一个(waitSet)可创建多个 Condition
唤醒notify / notifyAllsignal / signalAll
超时支持(wait(long ms))支持(await(long, TimeUnit))
不可中断不支持支持(awaitUninterruptibly())
底层机制JVM monitorAQS park/unpark
第 8 站

AQS 家族:CountDownLatch、Semaphore、ReadWriteLock

理解了 AQS 三板斧,其他工具迎刃而解——差异仅在于state 的语义tryAcquire/tryRelease 的实现

CountDownLatch.Sync · 核心逻辑
Sync(int count) { setState(count); }  // state = 初始计数值
protected int tryAcquireShared(int arg) {
    return (getState() == 0) ? 1 : -1;  // state==0 成功,否则阻塞
}
protected boolean tryReleaseShared(int arg) {
    for (;;) {
        int c = getState();
        if (c == 0) return false;
        if (compareAndSetState(c, c - 1))
            return c - 1 == 0;  // 减到 0 才释放所有等待线程
    }
}
Semaphore.NonfairSync · 核心逻辑
protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;  // <0 表示许可不够,获取失败
    }
}
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState(), next = c + releases;
        if (compareAndSetState(c, next)) return true;
    }
}
ReentrantReadWriteLock.Sync · state 位拆分
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  // 0x0000FFFF
// 读锁计数 = state 高 16 位(无符号右移)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁计数 = state 低 16 位(位与掩码)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
ReadWriteLock 为什么拆高 16 位和低 16 位?

在同一个 state 中同时记录读写锁持有数,只需一次 CAS 操作就能完成状态变更,避免维护两个独立变量的一致性问题和额外同步开销。代价是每种锁最多支持 65535 次重入。

工具state 语义模式tryAcquire 逻辑
ReentrantLock0=空闲,>0=重入次数独占state==0 则 CAS;持有者则 state+1
CountDownLatch倒计时计数共享state==0 成功,否则阻塞
Semaphore可用许可数共享state >= acquires 则 CAS 减少
ReadWriteLock高16位=读,低16位=写独占+共享写锁:无读无写则CAS;读锁:无写锁则CAS增加高位

除了上述四个经典工具,ThreadPoolExecutor 的 Worker 也内部继承了 AQS,实现了一个不可重入的独占锁(state 只有 0 和 1),用于保护工作线程的状态。FutureTask 的 Sync 同样基于 AQS,state 表示任务生命周期(NEW → COMPLETING → NORMAL/CANCELLED/EXCEPTIONAL)。AQS 是整个 JUC 包真正的"地基"。

第 9 站

总结:AQS 的设计哲学与全景图

AQS 的设计哲学归结为三个核心原则:

  • 状态驱动:所有同步语义通过一个 volatile int state 表达,CAS 保证原子修改
  • 队列兜底:获取失败的线程统一进入 CLH 变体队列,park/unpark 管理生命周期,避免忙等
  • 模板方法:AQS 定义 acquire/release 骨架,子类只需重写 tryAcquire/tryRelease 实现定制语义

模板方法模式是 AQS 设计的精髓所在。AQS 将不变的排队、park、unpark 逻辑固化在父类中(acquire、release、acquireQueued),将可变的同步语义开放给子类(tryAcquire、tryRelease)。这正是开闭原则(Open-Closed Principle)的经典体现——对扩展开放,对修改关闭。新增一个同步工具,不需要理解复杂的队列管理和线程调度细节,只需定义"什么是获取成功"和"什么是释放成功"即可。Doug Lea 用这套设计,将并发编程中最容易出错的底层细节全部封装在了 AQS 内部。

AQS 工具全景图:同一框架,不同语义 AbstractQueuedSynchronizer volatile state + CLH 队列 + 模板方法 ReentrantLock 独占 | state=重入计数 CountDownLatch 共享 | state=倒计时 Semaphore 共享 | state=许可数 ReadWriteLock 独占+共享 | state 拆高低位 一个 AQS,多种同步语义 差异仅在 state 的含义和 tryAcquire/tryRelease 的实现
图 4AQS 工具全景图:所有同步工具共享同一个底层框架
ReentrantLock vs synchronized 选型指南
synchronized:JVM 层面实现,自动释放,代码简洁,JDK 6 后性能大幅提升(偏向锁、轻量级锁、锁升级)
ReentrantLock:API 层面实现,需手动 unlock(),但提供公平锁、可中断、超时、Condition 等高级功能

面试回答模板

Q:请介绍 AQS 的原理以及 ReentrantLock 的实现。

"AQS 是 JUC 包的核心框架,内部维护 volatile int state 和 CLH 改造的双向 FIFO 队列。AQS 采用模板方法模式,子类重写 tryAcquire/tryRelease 实现不同同步语义。

ReentrantLock 基于 AQS 实现独占锁。state=0 空闲,>0 重入次数。lock() 先 CAS(0→1),失败则 tryAcquire(含重入检查),仍失败则入 CLH 队列 park。unlock() 时 state-1,减到 0 释放并 unparkSuccessor 唤醒后继。

公平锁在 tryAcquire 中多了 hasQueuedPredecessors() 检查,保证 FIFO 顺序;非公平锁允许直接 CAS 插队,减少上下文切换,吞吐量更高。绝大多数场景用非公平即可。

Condition 通过独立条件队列实现 wait/notify。await() 释放锁入条件队列 park,signal() 转移节点到 sync 队列并 unpark。支持多个 Condition 实现精确唤醒。"

Q:公平锁和非公平锁如何选择?

"绝大多数场景用非公平锁(默认),性能更好。只有需要严格保证不饥饿或业务要求 FIFO 顺序时才用公平锁。"