AQS 到底怎么排队的?CLH 队列、state 与条件变量全链路拆解

AQS 到底怎么排队的?CLH 队列、state 与条件变量全链路拆解

面试常考:AQS 独占/共享模式、CLH 队列结构、state 同步状态、ConditionObject

一、从一个排队问题出发

ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock------这些并发工具底层都依赖同一个框架:AQS(AbstractQueuedSynchronizer) 。AQS 是怎么管理线程排队的?state 到底控制什么?CLH 队列怎么运作?

二、AQS 全景架构

objectivec 复制代码
┌──────────────────────────────────────────────────────────────┐
│                        AQS 架构                              │
│                                                              │
│    ┌────────────────────────────────────────┐               │
│    │        volatile int state              │  ← 同步状态   │
│    │        (CAS 修改)                       │               │
│    └────────────────────────────────────────┘               │
│                         │                                    │
│    ┌────────────────────▼───────────────────┐               │
│    │     exclusiveOwnerThread               │  ← 独占线程   │
│    │     (继承自 AbstractOwnableSynchronizer)│               │
│    └────────────────────────────────────────┘               │
│                                                              │
│    ┌────────────────────────────────────────┐               │
│    │          CLH 等待队列                   │               │
│    │                                        │               │
│    │   head          tail                   │               │
│    │    │              │                    │               │
│    │    ▼              ▼                    │               │
│    │  ┌──────┐  ┌──────┐  ┌──────┐         │               │
│    │  │Node  │←▶│Node  │←▶│Node  │         │               │
│    │  │(哨兵)│  │Thread│  │Thread│         │               │
│    │  │ws=SIGNAL│ws=SIGNAL│ws=0  │        │               │
│    │  └──────┘  └──────┘  └──────┘         │               │
│    └────────────────────────────────────────┘               │
│                                                              │
│    ┌────────────────────────────────────────┐               │
│    │     ConditionObject 等待队列            │               │
│    │   ┌──────┐  ┌──────┐  ┌──────┐        │               │
│    │   │Node  │←▶│Node  │←▶│Node  │        │               │
│    │   │Thread│  │Thread│  │Thread│        │               │
│    │   └──────┘  └──────┘  └──────┘        │               │
│    │         firstWaiter    lastWaiter     │               │
│    └────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────┘

三、核心字段

java 复制代码
public abstract class AbstractQueuedSynchronizer
        extends AbstractOwnableSynchronizer {

    // 同步状态,volatile 保证可见性
    private volatile int state;

    // CLH 队列头节点(哨兵节点,不存储线程)
    private transient volatile Node head;

    // CLH 队列尾节点
    private transient volatile Node tail;

    // 独占锁持有线程(父类 AbstractOwnableSynchronizer)
    // private transient Thread exclusiveOwnerThread;
}

state 的含义因实现而异

实现 state 含义 获取方式
ReentrantLock 重入次数 (0=空闲, >0=持有) 独占
Semaphore 剩余许可数 共享
CountDownLatch 剩余计数 共享
ReentrantReadWriteLock 高16位=读锁数, 低16位=写锁重入数 读写分离

四、Node:队列节点结构

java 复制代码
static final class Node {
    // 模式标记
    static final Node SHARED = new Node();    // 共享模式
    static final Node EXCLUSIVE = null;       // 独占模式

    // 等待状态
    static final int CANCELLED =  1;   // 节点取消
    static final int SIGNAL    = -1;   // 后继节点需要被唤醒
    static final int CONDITION = -2;   // 在 Condition 队列中
    static final int PROPAGATE = -3;   // 共享模式传播唤醒

    volatile int waitStatus;      // 等待状态
    volatile Node prev;           // 前驱节点
    volatile Node next;           // 后继节点
    volatile Thread thread;       // 等待的线程
    Node nextWaiter;              // Condition 队列的下一个节点

    // 前驱节点(CLH 核心设计:每个节点监听前驱状态)
    final Node predecessor() {
        Node p = prev;
        return (p == null) ? null : p;
    }
}

为什么叫 CLH 队列?

AQS 的队列是 CLH(Craig, Landin, and Hagersten)队列的变种。原始 CLH 队列只关注前驱节点,每个节点自旋检查前驱是否释放锁 。AQS 改为阻塞式:当前驱释放锁时唤醒后继。

bash 复制代码
原始 CLH:       每个节点自旋看前驱
AQS 变种:       前驱释放后 LockSupport.unpark(后继线程)

  head                              tail
   │                                 │
   ▼                                 ▼
 ┌──────┐  next  ┌──────┐  next  ┌──────┐
 │      │───────▶│      │───────▶│      │
 │ 哨兵 │        │Thread│        │Thread│
 │      │◀───────│      │◀───────│      │
 └──────┘  prev  └──────┘  prev  └──────┘
                  │
                  └── 每个节点的前驱释放锁时被 unpark 唤醒

五、独占模式:获取与释放

这是 ReentrantLock 的核心路径。

acquire:获取独占锁

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

三个步骤:

  1. tryAcquire:尝试获取(子类实现),成功直接返回
  2. addWaiter:失败则加入 CLH 队列尾部
  3. acquireQueued:在队列中自旋/阻塞等待获取

addWaiter:入队

java 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(mode);  // Thread.currentThread()

    // 快速尝试在尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速失败 → enq 自旋入队
    enq(node);
    return node;
}

private Node enq(Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 队列为空,初始化哨兵节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued:队列中等待获取

java 复制代码
final boolean acquireQueued(Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();  // 前驱节点

            // 前驱是 head 且 tryAcquire 成功
            if (p == head && tryAcquire(arg)) {
                setHead(node);    // 自己成为新的 head(哨兵)
                p.next = null;    // 帮助 GC
                failed = false;
                return interrupted;
            }

            // 判断是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 根据前驱状态决定是否阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱已设置 SIGNAL,可以安全阻塞
        return true;
    if (ws > 0) {
        // 前驱已取消,跳过取消节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前驱状态为 0 或 PROPAGATE,CAS 设为 SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  // 阻塞当前线程
    return Thread.interrupted();  // 被唤醒后检查中断状态
}

获取锁完整流程图:

scss 复制代码
            ┌──────────────────┐
            │  acquire(arg)    │
            └────────┬─────────┘
                     │
              ┌──────▼───────┐
              │ tryAcquire?  │──成功──▶ 返回
              └──────┬───────┘
                 失败 │
              ┌──────▼───────┐
              │ addWaiter()  │  加入CLH队列尾部
              └──────┬───────┘
                     │
              ┌──────▼───────────────┐
              │ 前驱==head &&        │
              │ tryAcquire成功?      │──成功──▶ setHead, 返回
              └──────┬───────────────┘
                 失败 │
              ┌──────▼───────────────┐
              │ shouldParkAfter      │
              │ FailedAcquire()?     │
              └──────┬───────────────┘
                    │
              ┌─────▼──────┐
              │ park阻塞   │  LockSupport.park()
              │ 等待前驱   │
              │ 唤醒       │
              └─────┬──────┘
                    │ 被unpark唤醒
                    │
              ┌─────▼──────┐
              │ 再次尝试   │◀──── for 循环重试
              │ tryAcquire │
              └────────────┘

release:释放独占锁

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 子类实现
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 唤醒后继节点
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 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);  // 唤醒后继线程
}

为什么从尾部向前查找? 因为 addWaiternode.prev = pred 先于 pred.next = node 设置。从尾部向前遍历保证能找到所有有效节点。

六、共享模式

Semaphore、CountDownLatch 使用共享模式。

java 复制代码
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);  // 返回值: >=0成功
                if (r >= 0) {
                    setHeadAndPropagate(node, r);  // 传播唤醒
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 共享模式特有:传播唤醒
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);
    // propagate > 0 或 head 状态需要传播 → 唤醒后继共享节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

为什么共享模式需要传播唤醒?

java 复制代码
Semaphore permits=3:
  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
  │ 哨兵 │→│ T1   │→│ T2   │→│ T3   │
  └──────┘  └──────┘  └──────┘  └──────┘

线程释放1个许可 → 唤醒T1
T1获取成功,permits还剩 → 传播唤醒T2
T2获取成功,permits还剩 → 传播唤醒T3

七、ReentrantLock 的 tryAcquire 实现

java 复制代码
// ReentrantLock 的 NonfairSync (非公平)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 空闲,CAS 获取
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入
        int nextc = c + acquires;
        if (nextc < 0) throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// FairSync (公平)
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()) {
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}
维度 非公平锁 公平锁
获取时 直接 CAS 抢 先检查队列
吞吐量
饥饿 可能 不会
适用场景 大多数场景 严格 FIFO

八、ConditionObject:条件变量

java 复制代码
public class ConditionObject implements Condition {
    private transient Node firstWaiter;  // 条件队列头
    private transient Node lastWaiter;   // 条件队列尾
}

Condition 维护一个独立于 CLH 队列 的单向等待队列。调用 await() 的线程从 CLH 队列移到 Condition 队列,调用 signal() 时从 Condition 移回 CLH 队列。

swift 复制代码
await/signal 流程:
┌─────────────────────────────────────────────────────────┐
│                                                         │
│  CLH同步队列                      Condition等待队列       │
│  ┌──────→───→───┐                  ┌───→───→───┐       │
│  │Head  Node  Node│                │ W1   W2   W3│      │
│  │      │     │  │                 │     │     │  │     │
│  └──────←───←───┘                  └───←───←───┘       │
│                                                         │
│  await():  当前线程从CLH移至Condition尾部, 释放锁, 阻塞  │
│  signal(): Condition队首移至CLH尾部, 等待重新获取锁     │
│                                                         │
└─────────────────────────────────────────────────────────┘

await 核心实现

java 复制代码
public final void await() throws InterruptedException {
    // 1. 创建 Condition 节点加入等待队列
    Node node = addConditionWaiter();
    // 2. 释放锁(回到 state=0),保存之前的 state
    int savedState = fullyRelease(node);
    // 3. 阻塞,直到被 signal 或中断
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4. 被 signal 后重新在 CLH 队列中竞争锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    ...
}

signal 实现

java 复制代码
public final void signal() {
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 从 Condition 队列移除
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&  // 移入 CLH 队列
             (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
    // CAS 状态从 CONDITION 改为 0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 加入 CLH 队列尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    // 前驱设为 SIGNAL,必要时直接唤醒
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

用 Condition 实现生产者-消费者

java 复制代码
public class BoundedBuffer<T> {
    private final Object[] items;
    private int putIdx, takeIdx, count;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedBuffer(int capacity) {
        items = new Object[capacity];
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();    // 队列满,等待
            items[putIdx] = item;
            if (++putIdx == items.length) putIdx = 0;
            count++;
            notEmpty.signal();      // 通知消费者
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();   // 队列空,等待
            T item = (T) items[takeIdx];
            items[takeIdx] = null;
            if (++takeIdx == items.length) takeIdx = 0;
            count--;
            notFull.signal();       // 通知生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

九、面试高频问题速答

Q1: AQS 的 CLH 队列和原始 CLH 队列有什么区别?

原始 CLH 队列每个节点自旋检查前驱状态,不阻塞。AQS 改为阻塞式:获取失败的线程 LockSupport.park 挂起,前驱释放后 unpark 唤醒后继。AQS 还在 Node 中增加了 nextWaiter(Condition链)、waitStatus(状态标记)等字段。

Q2: AQS 为什么用 CLH 变种而不是普通队列?

CLH 易于实现取消和超时,只需修改前驱的 waitStatus;入队和出队只需 CAS 更新 tail/head,并发控制简单;每个节点只需关注前驱,天然支持取消节点的清理。

Q3: state 为什么是 volatile + CAS 而不是直接用 synchronized?

volatile 保证可见性,CAS 保证原子性,组合起来是无锁同步。避免了 synchronized 的内核态切换开销,在低竞争场景下性能更好。

Q4: 公平锁和非公平锁的区别是什么?

非公平锁 tryAcquire 直接 CAS 抢锁,不管队列中是否有等待者。公平锁先调用 hasQueuedPredecessors() 检查队列。非公平锁吞吐量高(减少线程切换),但可能导致队列中的线程长时间拿不到锁。

Q5: Condition 的 await/signal 和 Object 的 wait/notify 有什么区别?

维度 Object wait/notify Condition await/signal
依赖 synchronized Lock
队列数 1个 多个(每个 Condition 一个)
精度 notify 随机唤醒 signal 唤醒指定条件
中断 不支持不响应中断 支持响应/不响应中断
超时 支持 支持 + 纳秒级

Q6: AQS 中为什么 unparkSuccessor 要从尾部向前找?

入队时 enq 方法先设置 node.prev,再 CAS 设置 tail,最后设置 pred.next。如果从头部向后找,可能遇到 pred.next 还没设置的情况导致漏掉节点。从尾部向前找 prev 是安全的,因为 prev 一定在入队时就设置好了。

十、总结

AQS 是 JUC 的基石,核心设计:

  • state + CAS:无锁状态管理,不同子类赋予不同语义
  • CLH 变种队列:线程安全入队,阻塞/唤醒式等待,前驱释放后唤醒后继
  • 独占/共享分离:独占模式一次一个线程,共享模式支持传播唤醒
  • ConditionObject:每个 Condition 独立等待队列,实现精确唤醒

理解了 AQS,ReentrantLock、Semaphore、CountDownLatch 的源码就是 "state 的不同语义 + tryAcquire/tryRelease 的不同实现" 而已。AQS 让你站在 JUC 之上看到统一的设计模式。

相关推荐
Json____39 分钟前
springboot开发高校选课管理系统:从零构建前后端分离的全栈实践
java·spring boot·后端·毕业设计·毕设·wwwoop.com
xcl09251 小时前
民宿预约系统开发实战:从需求分析到上线部署全流程解析
java·spring boot
步行cgn1 小时前
为何以继承方式引入SpringBoot
java·spring boot·后端
CoderYanger1 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
禾高网络1 小时前
养老系统开发:背景、架构与养老行业展望
java·大数据·人工智能·小程序·架构
CCYe、2 小时前
新模型上线、旧模型下线:企业AI网关如何管住模型版本
java·网络·数据库·人工智能
风筝在晴天搁浅2 小时前
解题代码清爽版
java·开发语言
NeverFear7922 小时前
【黑马点评】短信登录
java·redis·tomcat·intellij-idea
2601_962298672 小时前
正则表达式引擎 哪些
java·python·perl·pcre·正则表达式引擎