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 队列包含了 headtail 两个指针。

  • 初始化时: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)
    • 该方法主要作用是修改前驱节点 pwaitStatus
    • 如果 p.waitStatusSIGNAL,说明前驱已经知道要唤醒我了,返回 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. 子类负责轻量的业务语义映射。只需要实现 tryAcquiretryRelease 等特定方法,告诉 AQS "如何判断抢锁成功"以及"如何修改 State"即可。


8. 总结

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

  1. 原子性 :依赖 CAS 操作 state 和队列指针。
  2. 阻塞 :依赖 LockSupport.park/unpark,避免了 Object.wait 对 monitor 锁的依赖。
  3. 队列 :采用 CLH 变体,通过前驱节点的 waitStatus 来决定当前节点是阻塞还是唤醒,精妙地解决了并发下的自旋与上下文切换开销问题。
相关推荐
SweetCode15 小时前
【无标题】
开发语言·c++·算法
shughui15 小时前
Python基础面试题:语言定位+数据类型+核心操作+算法实战(含代码实例)
开发语言·python·算法
No0d1es16 小时前
2025年12月电子学会青少年软件编程Python六级等级考试真题试卷
开发语言·python·青少年编程·等级考试·电子学会
zlp199216 小时前
xxl-job java.sql.SQLException: interrupt问题排查(二)
java·开发语言
sunnyday042616 小时前
深入理解Java日志框架:Logback与Log4j2配置对比分析
java·log4j·logback
superman超哥16 小时前
Rust HashSet与BTreeSet的实现细节:集合类型的底层逻辑
开发语言·后端·rust·编程语言·rust hashset·rust btreeset·集合类型
浩瀚地学16 小时前
【Java】异常
java·开发语言·经验分享·笔记·学习
张np16 小时前
java基础-LinkedHashMap
java·开发语言
gihigo199816 小时前
基于MATLAB的周期方波与扫频信号生成实现(支持参数动态调整)
开发语言·matlab