Java 并发基石:AQS (AbstractQueuedSynchronizer)

Java 并发基石:AQS (AbstractQueuedSynchronizer)

1. 引言

在 Java java.util.concurrent (JUC) 包中,锁(Lock)、信号量(Semaphore)、倒计数器(CountDownLatch)等高频并发工具类的背后,都有一个共同的基石------AbstractQueuedSynchronizer(简称 AQS)。

AQS 是由 Doug Lea 大神设计的一个用来构建锁和同步器的框架。它屏蔽了复杂的线程阻塞、唤醒机制以及等待队列的管理,通过模板方法模式,让上层应用只需实现少量的特定方法即可构建功能强大的同步组件。


2. AQS 核心架构概览

AQS 的核心思想是将并发控制逻辑抽象为对一个共享资源状态(state)的维护,以及对竞争资源失败的线程进行排队管理。

2.1 核心组件

AQS 类定义中最重要的几个字段如下:

java 复制代码
private volatile int state; // 同步状态
private transient volatile Node head; // 队列头(哨兵或持有锁的节点)
private transient volatile Node tail; // 队列尾
private transient Thread exclusiveOwnerThread; // (继承自 AOS) 独占模式下的持有线程

1. 同步状态 (state)

  • 本质 :state 是所有并发逻辑的判断依据,必须是 volatile 的,配合 Unsafe.compareAndSwapInt (CAS) 实现原子性修改。

  • 工作机制 :AQS 框架本身并不关心 state 具体等于几,它只提供 CAS 方法。

    对于 ReentrantLock,state 表示锁的重入次数(0表示无锁)。

    对于 Semaphore,state 表示剩余许可数量。

    对于 CountDownLatch,state 表示剩余的计数。

    • 抢锁:本质就是 CAS(state, 0, 1)(以 ReentrantLock 为例)。CAS 成功,CPU 缓存行同步,线程获得执行权;CAS 失败,说明有竞争,线程准备入队。
    • 内存可见性 :根据 JMM(Java 内存模型)的 happens-before 规则,对 volatile 变量的写操作先行发生于后续的读操作。这意味着:线程释放锁修改 state 之前的所有操作,对后续拿到锁读取 state 的线程都是可见的。这是 AQS 保证线程安全的基础。

2. CLH 变体队列

  • 设计痛点:如果成千上万个线程争抢一个锁,直接 CAS 会导致总线风暴(Bus Storm)。AQS 需要一个队列让失败的线程"排队睡觉"。
  • 结构特性 :双向链表(Node)。
    • Head(头节点) :严格来说,Head 指向的节点代表当前持有锁的线程(或者是刚刚释放锁、正在唤醒后继的节点)。它是一个"哨兵",不存储具体的等待线程信息。
    • Tail(尾节点):新来的请求失败线程,通过 CAS 操作拼接到 Tail 后面。
  • 核心逻辑 :
    • 前驱负责唤醒后继 :AQS 的队列运行机制是高度被动 的------当前节点无法自我唤醒,全靠前驱节点(Predecessor)在释放资源时传递信号。因此,维护"唤醒链条"的连通性 至关重要。当一个线程获取锁失败并入队后,在它真正调用 LockSupport.park() 挂起自己之前,必须执行一个关键的**"前驱探查"**动作(源码中的 shouldParkAfterFailedAcquire 方法):
      • 剔除死链(Skip Cancelled) :当前线程显然是活跃的(正在运行代码),但它的前驱节点可能因为超时或中断而处于 CANCELLED(放弃)状态。一个已废弃的节点永远不会执行唤醒操作,如果直接排在它后面,当前线程将面临"永久阻塞"的风险。
        因此,当前节点会检查前驱的 waitStatus,如果是 CANCELLED,则修改 prev 指针跳过 该节点,并循环向前查找,直到找到一个有效且正常 的节点挂在它后面。这种由后继者负责清理前驱的机制,实现了对队列中废弃节点的惰性移除 (Lazy Removal),确保了链表的活性。
      • 建立信号契约(Signal Setup) :找到有效前驱后,当前节点需要利用 CAS 将前驱的状态修改为 SIGNAL (-1)。。只有完成了这个信号契约,当前线程才会安心地调用 park() 进入阻塞状态。
    • 只有当前驱节点释放锁时,才会定向唤醒(unpark)它的直接后继节点。这种点对点的唤醒避免了所有等待线程同时醒来抢锁。

3. 持有者线程 (exclusiveOwnerThread)

  • 作用:仅用于独占模式。
  • 逻辑闭环 :当线程尝试获取锁时,会判断 current_thread == exclusiveOwnerThread。
    • 如果相等,说明是重入 ,不需要 CAS 抢锁,直接 state++ 即可。
    • 这是 ReentrantLock 能够实现递归调用的底层支撑。

2.2 资源共享模式

这两种模式的区别不仅仅是"一个线程"和"多个线程",真正的区别在于唤醒后的行为(Propagation)。

1. Exclusive (独占模式)

  • 代表 :ReentrantLock
  • 行为 :单点唤醒 。
    • 持有锁的线程释放锁 (release) -> 唤醒 Head 的后继节点 (Next) -> Next 醒来抢到锁 -> Next 执行。
    • 整个过程是一对一的接力。

2. Share (共享模式)

  • 代表 :CountDownLatch, Semaphore
  • 行为 :级联传播 (Propagation) 。
    • 这就是共享模式最难理解的地方。当一个节点在共享模式下被唤醒并成功获取资源(比如 state 从 0 变为 1,或者 Semaphore 还有剩余许可)后,它不仅自己醒来执行,还会判断是否需要唤醒它的后继节点。
    • 源码逻辑 :setHeadAndPropagate()。如果资源还有剩余(tryAcquireShared返回值 > 0),当前节点会继续触发 doReleaseShared(),唤醒下一个节点。这就形成了一个多米诺骨牌效应 ,使得多个等待线程可以在瞬间几乎同时被唤醒(例如 CountDownLatch 倒计时归零时)。

3. 内部数据结构:Node 节点

CLH 队列中的节点由内部类 Node 定义。理解 Node 的字段及其状态是理解 AQS 流程的关键。

java 复制代码
static final class Node {
    // 模式定义
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 线程等待状态 (waitStatus) 常量
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    // 当前节点的状态
    volatile int waitStatus;

    // 前驱指针
    volatile Node prev;

    // 后继指针
    volatile Node next;

    // 封装的线程
    volatile Thread thread;

    // 指向下一个处于 Condition 等待的节点(用于 ConditionObject)
    Node nextWaiter;
    
    // ...
}

3.1 waitStatus 详解

waitStatus 初始化为 0,表示普通同步节点。其他状态含义如下:

  • CANCELLED (1):当前节点内的线程因为超时或中断被取消。该状态是终态,一旦设置不会改变。
  • SIGNAL (-1) :非常关键的状态 。表示当前节点的后继节点处于阻塞(park)状态。当前节点在释放或取消时,必须唤醒(unpark)它的后继节点。
  • CONDITION (-2):节点处于条件队列(Condition Queue)中,等待被 signal。
  • PROPAGATE (-3):仅用于共享模式,表示下一次共享式同步状态获取将会无条件传播下去。

3.2 队列结构图示

AQS 队列包含了 head 和 tail 两个指针。

  • 初始化时:head 和 tail 指向同一个空的 Dummy Node(哨兵节点)。
  • 入队:新节点利用 CAS 插到 tail 后面。

4. 独占模式源码解析 (Acquire & Release)

我们以独占模式(如 ReentrantLock.lock())为例,完整剖析线程从尝试获取锁到阻塞,再到被唤醒的完整闭环。

4.1 获取资源:acquire(int arg)

这是 AQS 的顶层入口方法,采用模板方法模式:

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

逻辑:

  1. tryAcquire(arg) :尝试直接获取资源。这是一个抽象方法,由子类实现(如 ReentrantLock 里的非公平锁实现直接 CAS state)。如果成功,直接返回。
  2. addWaiter(Node.EXCLUSIVE):如果获取失败,将当前线程封装成 Node,加入等待队列尾部。
  3. acquireQueued(node, arg):节点入队后,进入自旋+阻塞逻辑。
  4. selfInterrupt():如果在等待过程中被中断过,获取锁后补一个中断信号。
4.1.1 入队逻辑:addWaiter

如果 tryAcquire 失败,线程需要进入等待队列。AQS 采用 CLH 变体队列 (双向链表),新节点总是插入到 tail 后面。

java 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 快速路径:如果队列不为空,尝试一次CAS加到队尾
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 慢速路径:队列为空或CAS失败,进入自旋重试
    enq(node);
    return node;
}

enq(node) 方法通过死循环(自旋)保证节点一定能插入队尾。

注意 :首次入队时,队列是空的(head 和 tail 都是 null)。enq 方法会先创建一个空的 Dummy Node (哨兵节点)作为 head,然后再把当前线程的节点接在后面。因此,队列中永远至少有一个节点。

4.1.2 挂起核心:acquireQueued

这是 AQS 最复杂也是最核心的部分。

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前驱
            final Node p = node.predecessor();
            
            // 只有前驱是 head 时,才有资格尝试获取锁
            // 因为 head 是持有锁的节点(或者刚释放完),轮也轮到 head 的下一个了
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,自己成为 head (dummy)
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            // 获取失败,判断是否需要挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

关键点解析:

  1. 自旋检查 :并不是一入队就立刻阻塞。如果前驱是 Head,会再次尝试 tryAcquire。这是为了提升性能,也许前一个线程刚刚释放锁。
  2. shouldParkAfterFailedAcquire(p, node) :
    • 该方法主要作用是修改前驱节点 p 的 waitStatus。
    • 如果 p.waitStatus 是 SIGNAL,说明前驱已经知道要唤醒我了,返回 true。
    • 如果 p.waitStatus > 0(CANCELLED),说明前驱放弃了,向前查找直到找到一个正常的节点,挂在它后面。
    • 如果是 0 或 PROPAGATE,利用 CAS 将前驱状态置为 SIGNAL,返回 false(不阻塞,外层循环再试一次,确保 CAS 成功)。
  3. parkAndCheckInterrupt() :
    • 调用 LockSupport.park(this) 挂起当前线程。线程停在这里,不再消耗 CPU。
    • 当被 unpark 或中断时,从这里醒来,返回中断状态。

4.2 释放资源:release(int arg)

释放锁的逻辑相对简单,核心在于修改 State 并唤醒 Head 的后继。

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 子类实现,修改 state
        Node h = head;
        // 如果头节点存在且状态不为0(说明有后继需要唤醒)
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
4.2.1 唤醒后继:unparkSuccessor
java 复制代码
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 将 head 的状态置零,清除 SIGNAL 标记
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 如果 next 节点为空 或者 next 被取消了
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从 tail 向前遍历,找到离 head 最近的一个有效节点
        // 为什么要从后往前?因为 addWaiter 中是先设置 prev 再 CAS tail,
        // 最后才设置 pred.next。next 指针在并发下可能不一致或为空。
        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 向前遍历查找?

这与 addWaiter 中的入队逻辑有关。在节点入队时:

java 复制代码
node.prev = pred; // 1. 先设置 prev
if (compareAndSetTail(pred, node)) { // 2. CAS 设置 tail
    pred.next = node; // 3. 最后设置 next
}

这是一个非原子 的操作。在步骤 2 执行成功(tail 已更新)但步骤 3 还没执行(next 指针还没连上)的瞬间,如果此时执行唤醒逻辑,通过 head.next 是找不到新节点的。

但此时 node.prev 已经连接成功。因此,从 tail 通过 prev 指针向前遍历是绝对安全的,能保证不会漏掉刚刚入队的节点。

4.0 AQS 独占模式全流程图

图 1:入队阶段 (Acquire & AddWaiter)

描述线程尝试获取锁失败后,初始化队列(如果需要)并将自己封装为 Node 加入队尾的过程。


图 2:核心循环阶段 (AcquireQueued - 自旋与挂起)

这是 AQS 的心脏。描述节点入队后,如何在"尝试抢锁"和"阻塞睡觉"之间切换,以及如何处理"放弃"的前驱节点。

图 3:释放与唤醒阶段 (Release & Unpark)

描述持有锁的线程释放资源,并查找下一个合适的节点进行唤醒的过程。重点在于从后往前找的兜底逻辑。

5. 共享模式 (Shared) 的区别

共享模式(如 CountDownLatch)与独占模式的主要区别在于:当一个线程获取同步状态成功后,会继续传播唤醒等待在后面的共享节点。

5.1 doAcquireShared

流程大体一致,但在获取资源成功后,调用 setHeadAndPropagate(node, r)。

java 复制代码
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);
    // 如果 propagate > 0 (剩余资源 > 0) 或者旧/新 head 状态为 PROPAGATE/SIGNAL
    if (propagate > 0 || h == null || h.waitStatus < 0 || ... ) {
        Node s = node.next;
        // 如果后继是共享模式,继续唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

这就是为什么 CountDownLatch.countDown() 减到 0 时,所有 await() 的线程会几乎同时被唤醒的原因------级联唤醒。


6. ConditionObject 机制

AQS 的强大之处在于它不仅仅支持简单的"抢锁",还通过内部类 ConditionObject 实现了类似 Object.wait/notify 的高级同步语义。

要理解 Condition,必须首先建立 "双队列模型" 的认知。

6.1 同步队列 vs 条件队列

AQS 内部实际上存在两套队列系统,它们共用 Node 类,但组织方式和用途完全不同:

  1. 同步队列 (Sync Queue / CLH):

    • 用途:存储争抢锁失败的线程。
    • 结构 :双向链表 (prev + next)。
    • 状态 :关注 SIGNAL (-1) 和 CANCELLED (1)。
    • 并发:入队出队涉及激烈的 CAS 竞争。
  2. 条件队列 (Condition Queue):

    • 用途 :存储调用了 await() 等待某些条件的线程。
    • 结构 :单向链表 (nextWaiter)。
    • 状态 :关注 CONDITION (-2)。
    • 并发 :无锁竞争 。因为调用 await() 或 signal() 时,线程必须已经持有锁。
6.1.1 结构示意图

6.2 await()

当线程调用 condition.await() 时,执行:主动放弃锁 -> 进入条件队列 -> 等待被召回 -> 重新抢锁。

6.2.1 核心源码剖析
java 复制代码
public final void await() throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    
    // 1. 【入队】:将当前线程封装为 Node,加入条件队列尾部
    Node node = addConditionWaiter(); 
    
    // 2. 【彻底释放锁】:为什么要 releaseAll?
    // 因为 ReentrantLock 是可重入的,state 可能为 5。
    // await 时必须一次性把 state 减为 0,让出锁给别人,并保存 savedState 用于将来恢复。
    int savedState = fullyRelease(node); 
    
    int interruptMode = 0;
    
    // 3. 【挂起】:只要节点还在 Condition 队列中(没被 signal 移到 Sync 队列),就一直阻塞
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        // 处理中断唤醒的情况...
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 4. 【复活抢锁】:跳出 while 说明被 signal 了(或者被中断),
    // 此时节点已经回到了 Sync 队列。
    // 调用 acquireQueued 像普通线程一样排队抢锁,目标是将 state 恢复为 savedState。
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
        
    // ... 清理逻辑
}
6.2.2 关键步骤详解
  1. addConditionWaiter() :
    • 不需要 CAS!因为当前线程持有锁。直接操作 lastWaiter 指针将新节点挂到单向链表末尾,并将 waitStatus 设为 CONDITION (-2)。
  2. fullyRelease(node) :
    • 这是一个原子性的操作。如果失败(比如没拿锁就调 await),会抛出 IllegalMonitorStateException 并将节点状态置为 CANCELLED。
  3. isOnSyncQueue(node) :
    • 这是判断"我是谁?我在哪?"的关键。signal() 的本质是将节点从条件队列迁移到同步队列。
    • 如果 isOnSyncQueue 返回 false,说明还没有其它线程signal ,那继续 park 。

6.3 signal()

signal() 并不直接唤醒线程让它运行,而是将它从条件队列(Condition Queue)移到同步队列(Sync Queue)。

6.3.1 核心源码剖析
java 复制代码
public final void signal() {
    if (!isHeldExclusively()) // 必须持有锁
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 修改指针,将 first 从条件队列移除
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && // 核心转移逻辑
             (first = firstWaiter) != null);
}
6.3.2 核心转移逻辑:transferForSignal
java 复制代码
final boolean transferForSignal(Node node) {
    // 1. CAS 将节点状态从 CONDITION (-2) 改为 0
    // 如果失败,说明节点在 Condition 队列里已经取消了,返回 false 继续找下一个
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 2. 【入队】:调用 enq(node) 将节点放入 Sync 队列尾部
    // p 是 node 在 Sync 队列中的前驱
    Node p = enq(node); 
    
    int ws = p.waitStatus;
    
    // 3. 【可能唤醒】:
    // 这里的逻辑非常微妙!通常 signal 只是把节点移动到 Sync 队列,不唤醒。
    // 只有当:
    // a. 前驱节点 p 已经取消了 (ws > 0)
    // b. 或者无法设置前驱 p 的状态为 SIGNAL (无法让它负责唤醒我)
    // 这时候才迫不得已,直接 unpark 唤醒当前线程,让它自己去修正前驱状态。
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
        
    return true;
}

重点误区 :

绝大多数情况下,signal() 只是把节点从 Condition Queue 搬运到了 Sync Queue。线程依然在 await() 的 while 循环中阻塞着(或者刚被移过去,还没轮到它)。真正的唤醒通常发生在持有锁的线程调用 unlock() 时,通过 AQS 的正常释放流程唤醒 Sync Queue 中的 Head.next。


6.4 Condition 完整交互流程图

这张图展示了线程 A(等待者)和线程 B(唤醒者)如何在两个队列间配合。

6.5 总结 Condition 的设计哲学

  1. 条件谓词的原子性 :await 必须先释放锁再挂起,signal 必须持有锁,这保证了检查条件和操作等待队列是原子的,不会出现"丢失信号"的问题。
  2. 队列隔离 :将"等待条件"和"等待锁"分离。signal 的本质是将线程从"关注条件"的状态转换为"关注锁"的状态。
  3. 复用机制 :Condition 队列的节点被移到 Sync 队列后,完全复用了 AQS 原有的排队、阻塞、唤醒机制(acquireQueued)。

7. AQS 实战应用解析

AQS 的强大之处在于它将"锁"的概念泛化了。子类通过定义 state 的含义,并重写 tryAcquire / tryRelease 系列方法,即可实现形态迥异的同步器。

7.1 ReentrantLock:可重入的独占锁

ReentrantLock 的核心诉求是:排他性 与可重入性。

1. State 语义映射
  • State = 0 :自由态。表示当前锁未被任何线程持有。
  • State > 0 :锁定态 。数值表示当前持有线程的重入次数(Reentrancy Count)。
  • ExclusiveOwnerThread:必须记录是谁拿了锁,用于判断是否是重入。
2. tryAcquire (非公平锁实现)

非公平锁的特点是"抢占"。新来的线程不需要管队列里有没有人排队,直接尝试 CAS 改写 state。

java 复制代码
// ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    
    // 1. 自由态:直接 CAS 抢占
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current); // 标记"我"是主人
            return true; // 告诉 AQS:获取成功,不用入队
        }
    }
    // 2. 锁定态:检查是不是自己人(可重入逻辑)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // 计数累加
        if (nextc < 0) // 溢出检查
            throw new Error("Maximum lock count exceeded");
        setState(nextc); // 不需要 CAS,因为只有持有者能走到这,单线程操作
        return true;
    }
    // 3. 竞争失败
    return false; // 告诉 AQS:获取失败,把我放入队列阻塞吧
}
3. tryRelease (递归释放)

释放锁不仅仅是将 state 设为 0,而是做减法。只有减到 0 时,才算真正释放了物理锁。

java 复制代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 状态递减
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException(); // 防御性编程:没拿锁的人不能释放

    boolean free = false;
    // 只有减到 0,才意味着锁被彻底释放
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null); // 清空持有者
    }
    setState(c); // 更新 state
    return free; // 返回 true 才会触发 AQS 唤醒后继节点 (unparkSuccessor)
}

7.2 CountDownLatch:倒计时

CountDownLatch 是典型的共享模式 (Shared) 应用。它的逻辑是:count > 0 之前,所有人都要等;一旦 count == 0 ,所有人同时通过。

1. State 语义映射
  • State > 0 :闭锁关闭 。代表还需要等待 countDown 的次数。
  • State = 0 :闭锁开启。所有等待线程被释放,且新来的线程不会被阻塞。
2. await() -> tryAcquireShared

await() 方法内部调用的是 AQS 的 acquireSharedInterruptibly(1)。

这里的核心在于:它并不是要真的"获取"什么资源,而是判断"能不能通过"。

java 复制代码
protected int tryAcquireShared(int acquires) {
    // 这里的返回值含义:
    // < 0 : 获取失败,进入队列阻塞
    // >= 0 : 获取成功,通行
    
    // 逻辑极其简单:
    // 如果 state == 0,返回 1 (成功,且允许传播唤醒)
    // 如果 state > 0,返回 -1 (失败,进入队列阻塞)
    return (getState() == 0) ? 1 : -1;
}
3. countDown() -> tryReleaseShared

每次调用 countDown,state 减 1。关键点在于减到 0 的那一刻。

java 复制代码
protected boolean tryReleaseShared(int releases) {
    // 必须用死循环 CAS,因为可能有多个线程同时 countDown
    for (;;) {
        int c = getState();
        if (c == 0) // 已经归零了,无需再减
            return false;
            
        int nextc = c - 1;
        if (compareAndSetState(c, nextc)) {
            // 只有从 1 变成 0 的那个线程,返回 true
            // 这会触发 AQS 调用 doReleaseShared(),从而唤醒所有等待线程
            return nextc == 0;
        }
    }
}

7.3 Semaphore:流量控制的信号量

Semaphore 也是共享模式,但它与 CountDownLatch 不同。Latch 是一次性的,而 Semaphore 可以实现动态的增减。

1. State 语义映射
  • State :剩余许可证(Permits)数量。
  • 逻辑:线程获取许可,State 减 N;线程释放许可,State 加 N。
2. acquire() -> tryAcquireShared

这里展示了共享模式下如何处理"资源不足"和"并发竞争"。

java 复制代码
// 非公平模式实现
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        
        // 1. 如果 remaining < 0:
        //    说明资源不够,直接返回负数。AQS 会把线程放入队列。
        // 2. 如果 remaining >= 0:
        //    说明资源足够,尝试 CAS 修改 state。
        //    如果 CAS 成功,返回剩余量(>=0),代表获取成功。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

注意 :tryAcquireShared 的返回值不仅表示成功/失败,还表示**"剩余资源量"。如果返回值 > 0,AQS 的 setHeadAndPropagate 逻辑会尝试唤醒后继节点**,这对于提高 Semaphore 的吞吐量至关重要(因为可能还剩很多资源,应该让排队的后面几个人都醒过来)。

3. release() -> tryReleaseShared

释放就是简单的"归还资源",通过 CAS 增加 state。

java 复制代码
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // 溢出检查
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true; // 只要释放成功,就返回 true,AQS 会去唤醒等待线程
    }
}

7.4 子类实现的三个关键点

通过对比可以发现,实现自定义同步器主要关注三点:

  1. 定义 State:这个整数到底代表什么?(锁重入次数?倒计时?剩余许可证?)
  2. 定义成功与失败 :
    • 独占模式下,tryAcquire 返回 boolean。
    • 共享模式下,tryAcquireShared 返回 int(负数=失败,0=成功但不传播,正数=成功且传播)。
  3. 定义原子操作 :使用 compareAndSetState 循环处理状态变更,解决并发冲突。

**重点:**这套机制完美体现了 AQS "Template Method Pattern" (模板方法模式) 的精髓:

  1. AQS 负责复杂的排队和线程调度。比如:线程的排队(addWaiter)、阻塞(park)、唤醒(unparkSuccessor)以及队列维护。这些方法是 final 的,子类无法修改。

  2. 子类负责轻量的业务语义映射。只需要实现 tryAcquire、tryRelease 等特定方法,告诉 AQS "如何判断抢锁成功"以及"如何修改 State"即可。


8. 总结

AbstractQueuedSynchronizer 摒弃了传统的 synchronized 重量级锁依赖,通过 State (资源状态) + CLH Queue (线程等待队列) + LockSupport (阻塞机制) 构建了一套强大的同步框架。

  1. 原子性 :依赖 CAS 操作 state 和队列指针。
  2. 阻塞 :依赖 LockSupport.park/unpark,避免了 Object.wait 对 monitor 锁的依赖。
  3. 队列 :采用 CLH 变体,通过前驱节点的 waitStatus 来决定当前节点是阻塞还是唤醒,精妙地解决了并发下的自旋与上下文切换开销问题。
相关推荐
左手の明天7 分钟前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
shuoshuohaohao12 分钟前
MyBatis 小知识点
java·spring·mybatis
海上小飞龙25 分钟前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
朝朝辞暮i42 分钟前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
狗凯之家源码网1 小时前
淘客口令落地页生成器 PHP 版深度评测
开发语言·php
画绛集美术1 小时前
本地化部署LLM用于美术史问答:一间美术教室的实践记录
开发语言·php
谢亮_vipxieliang1 小时前
泛型擦除、通配符与 PECS 一次讲透
java·面试
八解毒剂1 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理
FakeOccupational2 小时前
【电路笔记 信号】DBPSK 波形查找表+脉冲成形(升余弦+根升余弦滤波)
开发语言·笔记
打工仔折腾 AI2 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战