面试必知必会(7):多线程AQS

Java面试系列文章

面试必知必会(1):线程状态和创建方式

面试必知必会(2):线程池原理

面试必知必会(3):synchronized底层原理

面试必知必会(4):volatile关键字

面试必知必会(5):CAS与原子类

面试必知必会(6):Lock接口及实现类

面试必知必会(7):多线程AQS


目录

  • 引言
  • 一、AQS核心定位与设计思想
    • [1 、什么是AQS?](#1 、什么是AQS?)
    • [2、核心设计思想:状态控制 + 队列管理](#2、核心设计思想:状态控制 + 队列管理)
  • 二、AQS底层核心结构(必懂细节)
  • 三、AQS核心流程:独占式与共享式(重中之重)
    • 1、独占式模式
      • [1.1、独占式获取资源:acquire(int arg)流程](#1.1、独占式获取资源:acquire(int arg)流程)
        • [步骤1:tryAcquire(int arg) ------ 尝试获取资源(子类自定义逻辑)](#步骤1:tryAcquire(int arg) —— 尝试获取资源(子类自定义逻辑))
        • [步骤2:addWaiter(Node mode) ------ 竞争失败,封装节点加入队列](#步骤2:addWaiter(Node mode) —— 竞争失败,封装节点加入队列)
        • [步骤3:acquireQueued(Node node, int arg) ------ 队列中阻塞等待,唤醒后重试](#步骤3:acquireQueued(Node node, int arg) —— 队列中阻塞等待,唤醒后重试)
        • [步骤4:selfInterrupt() ------ 恢复线程的中断状态](#步骤4:selfInterrupt() —— 恢复线程的中断状态)
      • [1.2、独占式释放资源:release(int arg)流程](#1.2、独占式释放资源:release(int arg)流程)
        • [步骤1:tryRelease(int arg) ------ 尝试释放资源(子类自定义逻辑)](#步骤1:tryRelease(int arg) —— 尝试释放资源(子类自定义逻辑))
        • [步骤2:unparkSuccessor(Node node) ------ 唤醒后继节点](#步骤2:unparkSuccessor(Node node) —— 唤醒后继节点)
    • 2、共享式模式
      • [2.1、共享式获取资源:acquireShared(int arg)流程](#2.1、共享式获取资源:acquireShared(int arg)流程)
        • [步骤1:tryAcquireShared(int arg) ------ 子类自定义共享获取逻辑](#步骤1:tryAcquireShared(int arg) —— 子类自定义共享获取逻辑)
        • [步骤2:doAcquireShared(int arg) ------ 加入队列,阻塞等待](#步骤2:doAcquireShared(int arg) —— 加入队列,阻塞等待)
      • [2.2、共享式释放资源:releaseShared(int arg)流程](#2.2、共享式释放资源:releaseShared(int arg)流程)
        • [步骤1:tryReleaseShared(int arg) ------ 子类自定义共享释放逻辑](#步骤1:tryReleaseShared(int arg) —— 子类自定义共享释放逻辑)
        • [步骤2:doReleaseShared() ------ 唤醒后续所有等待的共享节点](#步骤2:doReleaseShared() —— 唤醒后续所有等待的共享节点)

引言

在Java多线程并发编程中,我们经常会用到ReentrantLockCountDownLatchSemaphoreCyclicBarrier等同步工具,这些工具看似功能各异、互不相关,但它们的底层都依赖于同一个核心框架------AbstractQueuedSynchronizer(简称AQS)

一、AQS核心定位与设计思想

1 、什么是AQS?

**  AQS是Java并发包(java.util.concurrent.locks)中的一个抽象类,它并非一个具体的同步器,而是一个"同步器骨架"------它封装了同步状态的管理线程的排队等待、唤醒等核心共性逻辑,开发者只需重写少量自定义方法,就能快速实现一个符合自身需求的同步器(比如ReentrantLock就是AQS的子类实现)。**

简单来说,AQS的作用就是"统一解决多线程竞争共享资源时的排队、唤醒问题",避免每个同步工具都重复实现一套排队逻辑,极大简化了同步器的开发难度。

2、核心设计思想:状态控制 + 队列管理

AQS的设计精髓可以概括为一句话:通过一个共享状态变量(state)控制资源的访问权限,通过一个双向阻塞队列(CLH队列)管理竞争失败的线程。这种设计将"同步的共性逻辑"(排队、唤醒、状态原子操作)与"同步的个性逻辑"(是否允许获取资源、如何释放资源)分离,采用模板方法模式实现。

  • AQS抽象类:实现模板方法(如acquirerelease),封装排队、唤醒、状态原子操作等共性逻辑,不需要子类重写
  • 子类(如ReentrantLock):重写AQS的抽象方法(如tryAcquiretryRelease),定义"如何获取资源、如何释放资源"的个性逻辑,也就是自定义state的含义和操作规则

二、AQS底层核心结构(必懂细节)

1、共享状态变量:state(核心中的核心)

  • state 是 AQS 最关键的字段,表示当前同步器的状态
  • volatile修饰:保证state在多线程之间的内存可见性------当一个线程修改了state的值,其他线程能立即看到最新值,避免出现"线程可见性问题"(比如线程A修改了state,线程B却看到旧值,导致错误地获取资源)
  • 原子操作:修改state必须通过compareAndSetState方法(简称CAS),该方法依赖sun.misc.Unsafe类实现,底层是CPU的CAS指令,能保证"比较-修改"的原子性,避免多线程同时修改state导致的竞争问题
java 复制代码
// AQS核心状态变量,volatile保证多线程可见性
private volatile int state;

// 获取当前状态(子类可调用)
protected final int getState() {
    return state;
}

// 设置当前状态(仅在当前线程已获取独占锁时调用,无需CAS)
protected final void setState(int newState) {
    state = newState;
}

// 原子化修改state(核心方法,保证多线程下状态修改的原子性)
protected final boolean compareAndSetState(int expect, int update) {
    // 依赖Unsafe类的CAS操作,底层是CPU指令级别的原子操作
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
  • state的含义由子类定义:AQS本身不规定state的具体含义,完全由子类(同步器)决定,这也是AQS能适配多种同步场景的关键
    • ReentrantLock:state表示"锁的重入次数"------0表示无锁状态,≥1表示有线程持有锁(值为几,就表示重入了几次)
    • Semaphore:state表示"可用的许可数量"------线程获取资源时消耗1个许可(state减1),释放资源时归还1个许可(state加1)
    • CountDownLatch:state表示"倒计时计数器"------初始化时设置为N,线程调用countDown方法时state减1,直到state为0,所有等待的线程被唤醒
    • ReentrantReadWriteLock:state的高16位表示"读锁的持有数量",低16位表示"写锁的重入次数"------实现读写分离的同步控制

2、双向阻塞队列:CLH队列(线程排队的核心)

当多线程竞争共享资源时,必然会有线程竞争失败。此时,这些失败的线程不会立即退出,也不会一直自旋消耗CPU,而是会被AQS封装成一个"节点(Node)",加入到一个双向阻塞队列中等待,这个队列就是CLH队列(基于Craig, Landin, and Hagersten锁队列改进而来)。

CLH队列的核心特性是FIFO(先进先出),保证线程排队的公平性(当然,AQS也支持非公平模式,后续会讲);同时它是双向链表,每个节点都有前驱(prev)和后继(next)指针,方便节点的插入、删除和唤醒操作。

2.1、队列的核心组成:Node节点

CLH队列中的每一个节点,都对应一个等待资源的线程。AQS内部定义了一个静态内部类Node,封装了线程节点状态前驱/后继指针等信息。

java 复制代码
static final class Node {
    // 共享模式节点标记(如Semaphore、CountDownLatch)
    static final Node SHARED = new Node();
    // 独占模式节点标记(如ReentrantLock)
    static final Node EXCLUSIVE = null;

    // 节点状态:取消状态,线程已放弃等待(如超时、被中断)
    static final int CANCELLED =  1;
    // 节点状态:信号状态,表示当前节点的后继节点需要被唤醒
    static final int SIGNAL    = -1;
    // 节点状态:条件等待状态,节点在Condition队列中等待
    static final int CONDITION = -2;
    // 节点状态:传播状态,共享模式下,状态会向后传播(如CountDownLatch)
    static final int PROPAGATE = -3;

    // 当前节点的状态(volatile修饰,保证可见性)
    volatile int waitStatus;
    // 前驱节点(当前节点的前一个节点)
    volatile Node prev;
    // 后继节点(当前节点的后一个节点)
    volatile Node next;
    // 当前节点关联的等待线程
    volatile Thread thread;
    // 条件队列中的后继节点(用于Condition接口,后续讲解)
    Node nextWaiter;
}
  • Node节点的细节非常关键,尤其是节点状态(waitStatus),直接决定了节点的行为(是否需要被唤醒、是否要被移除队列)
    1. CANCELLED(1):取消状态。当线程等待超时、被中断,或者主动放弃等待时,节点会被标记为CANCELLED。处于该状态的节点,会被AQS从队列中移除,不再参与资源竞争,也不会被唤醒
    2. SIGNAL(-1):信号状态。表示当前节点的后继节点正在等待被唤醒,当前节点释放资源(或被取消)时,必须唤醒它的后继节点。这是最常用的状态,也是保证队列唤醒逻辑的核心------当一个节点加入队列尾部时,会将其前驱节点的状态设置为SIGNAL,确保前驱释放资源时能唤醒自己
    3. CONDITION(-2):条件等待状态。该状态仅用于"条件队列"(由Condition接口维护,后续讲解),表示节点正在等待某个条件(如await()方法),直到其他线程调用signal()方法,节点才会从条件队列转移到同步队列,参与资源竞争
    4. PROPAGATE(-3):传播状态。仅用于共享模式(如Semaphore、CountDownLatch),表示当前节点获取资源成功后,需要将"资源可用"的信号传播给后续节点,让后续节点也能尝试获取资源(比如CountDownLatch中,state为0时,所有等待节点都会被唤醒,就是利用了传播特性)
    5. 0(初始状态):节点被创建时的默认状态,无特殊含义。当节点被加入同步队列后,会根据场景切换到其他状态(如SIGNAL、CANCELLED)

2.2、队列的维护:head和tail指针

AQS通过两个volatile修饰的指针(head、tail)来维护CLH队列的头和尾。

java 复制代码
// 队列头节点(volatile修饰,保证多线程可见性)
private transient volatile Node head;
// 队列尾节点(volatile修饰,保证多线程可见性)
private transient volatile Node tail;
  1. head节点是"当前持有资源的线程节点"(空节点,不关联线程),仅作为队列的起始标记
  2. 新加入的线程会追加到 tail 之后,并更新 tail

三、AQS核心流程:独占式与共享式(重中之重)

AQS支持两种同步模式,对应不同的同步场景,这两种模式的流程是AQS的核心,也是面试高频考点。两种模式的核心区别在于:独占式模式下,同一时刻只有一个线程能获取资源;共享式模式下,同一时刻多个线程能同时获取资源

1、独占式模式

独占式模式是最常用的模式,ReentrantLock(公平锁、非公平锁)就是典型的独占式同步器。核心流程分为"获取资源(acquire)""释放资源(release)"两部分,AQS通过模板方法acquire(int arg)release(int arg)封装了核心逻辑。

1.1、独占式获取资源:acquire(int arg)流程

线程调用acquire(arg)方法获取独占资源,arg是获取资源所需的"数量"(比如ReentrantLock中arg=1,表示获取1个锁资源)。该方法是AQS的模板方法,源码如下(JDK 1.8):

java 复制代码
public final void acquire(int arg) {
    // 步骤1:尝试获取资源(tryAcquire由子类重写)
    // 步骤2:获取失败 → 加入同步队列(addWaiter)
    // 步骤3:在队列中阻塞等待,直到被唤醒后重新尝试获取资源(acquireQueued)
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        // 步骤4:如果线程被中断,执行中断自我唤醒
        selfInterrupt();
    }
}
步骤1:tryAcquire(int arg) ------ 尝试获取资源(子类自定义逻辑)

tryAcquire是AQS的抽象方法,由子类重写,核心作用是"判断当前线程是否能获取资源",返回true表示获取成功,返回false表示获取失败。AQS中该方法的默认实现是抛出异常,强制子类重写。

java 复制代码
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

这里以ReentrantLock的公平锁和非公平锁为例,讲解tryAcquire的具体实现(最经典的场景)

公平锁的tryAcquire实现(核心是"排队优先"

java 复制代码
// ReentrantLock公平锁的同步器实现(内部类FairSync)
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 1. 获取当前state状态
    int c = getState();
    // 2. 如果state=0,表示无锁状态,尝试获取锁
    if (c == 0) {
        // 关键:先判断队列中是否有等待的线程(hasQueuedPredecessors)
        // 没有等待线程 → CAS修改state为1,设置当前线程为锁持有者
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3. 如果state≠0,判断当前线程是否是锁的持有者(重入场景)
    else if (current == getExclusiveOwnerThread()) {
        // 重入:state累加(acquires=1,每次重入加1)
        int nextc = c + acquires;
        if (nextc < 0) // 防止重入次数溢出(int最大值是2^31-1)
            throw new Error("Maximum lock count exceeded");
        setState(nextc); // 已持有锁,无需CAS,直接修改state
        return true;
    }
    // 4. 其他情况(有锁且不是当前线程持有,或有等待线程)→ 获取失败
    return false;
}

公平锁的关键细节:hasQueuedPredecessors()方法------判断队列中是否有比当前线程更早等待的线程,如果有,当前线程不能插队,必须排队;如果没有,才能尝试CAS获取锁。这就是"公平"的核心含义。

非公平锁的tryAcquire实现(核心是"插队优先"

java 复制代码
// ReentrantLock非公平锁的同步器实现(内部类NonfairSync)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 关键区别:没有hasQueuedPredecessors()判断,直接尝试CAS获取锁
    if (c == 0) {
        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;
}

非公平锁的关键细节:当state=0(无锁)时,不管队列中是否有等待线程,当前线程都会直接尝试CAS获取锁------如果成功,就"插队"获取资源;如果失败,再加入队列排队。这种方式的优点是"吞吐量高"(减少线程上下文切换),缺点是"可能导致线程饥饿"(某些线程一直排队,无法获取资源)

步骤2:addWaiter(Node mode) ------ 竞争失败,封装节点加入队列

如果tryAcquire返回false(获取资源失败),线程会被封装成Node节点,加入到CLH队列的尾部。mode参数表示节点的模式(独占式:Node.EXCLUSIVE;共享式:Node.SHARED),源码如下(JDK 1.8):

java 复制代码
private Node addWaiter(Node mode) {
    // 1. 创建当前线程的Node节点(模式为独占式/共享式)
    Node node = new Node(Thread.currentThread(), mode);
    // 2. 尝试快速插入队列尾部(优化:先判断tail是否不为null,避免直接初始化队列)
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // CAS操作:将当前节点设置为新的tail
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3. 快速插入失败(tail为null,队列未初始化;或CAS失败)→ 初始化队列并插入节点
    enq(node);
    return node;
}
步骤3:acquireQueued(Node node, int arg) ------ 队列中阻塞等待,唤醒后重试

节点加入队列后,线程不会一直自旋,而是会进入"阻塞等待"状态,直到被前驱节点唤醒,唤醒后再重新尝试获取资源。acquireQueued方法就是实现这个逻辑的核心,源码如下(简化关键逻辑):

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 标记是否获取资源失败
    try {
        boolean interrupted = false; // 标记线程是否被中断
        // 自旋:不断检查自己是否能获取资源
        for (;;) {
            // 1. 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 2. 如果前驱节点是head(持有资源的节点),尝试获取资源
            if (p == head && tryAcquire(arg)) {
                // 3. 获取成功 → 将当前节点设为新的head(原head节点被移除队列)
                setHead(node);
                p.next = null; // 帮助GC回收原head节点
                failed = false;
                return interrupted; // 返回线程是否被中断
            }
            // 4. 获取失败 → 判断是否需要阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) {
                // 5. 如果线程被中断,标记interrupted为true
                interrupted = true;
            }
        }
    } finally {
        // 如果获取资源失败(比如异常),取消当前节点的等待
        if (failed) {
            cancelAcquire(node);
        }
    }
}
步骤4:selfInterrupt() ------ 恢复线程的中断状态

如果acquireQueued方法返回true(线程被中断),会调用selfInterrupt()方法,恢复线程的中断状态,源码如下:

java 复制代码
private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

1.2、独占式释放资源:release(int arg)流程

线程获取资源后,执行完业务逻辑,需要调用release(arg)方法释放资源,唤醒队列中等待的线程。该方法也是AQS的模板方法,源码如下(JDK 1.8):

java 复制代码
public final boolean release(int arg) {
    // 步骤1:尝试释放资源(tryRelease由子类重写)
    if (tryRelease(arg)) {
        // 步骤2:释放成功 → 获取当前head节点(持有资源的节点)
        Node h = head;
        // 步骤3:如果head不为null,且状态不是0 → 唤醒head的后继节点
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h);
        }
        return true;
    }
    // 释放失败(比如state未归零,或线程不是锁持有者)
    return false;
}
步骤1:tryRelease(int arg) ------ 尝试释放资源(子类自定义逻辑)

tryRelease是AQS的抽象方法,由子类重写,核心作用是"释放资源,修改state状态",返回true表示释放成功(资源完全释放,比如ReentrantLock的state归零),返回false表示释放失败(比如重入次数未归零)。

java 复制代码
// ReentrantLock的同步器实现(FairSync和NonfairSync共用)
protected final boolean tryRelease(int releases) {
    // 1. 释放资源:state减去释放的数量(releases=1)
    int c = getState() - releases;
    // 2. 检查当前线程是否是锁的持有者(只有持有者才能释放锁)
    if (Thread.currentThread() != getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    }
    boolean free = false;
    // 3. 如果state=0,表示资源完全释放(重入次数归零)
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null); // 清空锁持有者
    }
    // 4. 更新state(即使未完全释放,也要修改state的值)
    setState(c);
    return free; // 返回是否完全释放资源
}
  • 只有锁的持有者才能释放锁,否则会抛出IllegalMonitorStateException异常(比如线程A没持有锁,却调用release()方法)
  • 重入锁的释放:state会逐次减1,直到state=0,才表示资源完全释放(此时会清空锁持有者);如果state>0,说明还有重入次数,资源未完全释放,返回false,不会唤醒队列中的线程
步骤2:unparkSuccessor(Node node) ------ 唤醒后继节点

如果tryRelease返回true(资源完全释放),且head节点不为null、状态不为0,会调用unparkSuccessor方法,唤醒head节点的后继节点(队列中第一个等待的有效节点),源码如下:

java 复制代码
private void unparkSuccessor(Node node) {
    // 1. 获取当前节点(head)的状态
    int ws = node.waitStatus;
    // 2. 如果状态小于0(SIGNAL或PROPAGATE),将其设为0(表示已处理唤醒)
    if (ws < 0) {
        compareAndSetWaitStatus(node, ws, 0);
    }
    // 3. 获取当前节点的后继节点
    Node s = node.next;
    // 4. 如果后继节点为null,或状态为CANCELLED(无效节点)→ 从队列尾部向前找有效节点
    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; // 找到最前面的有效节点
            }
        }
    }
    // 5. 如果有有效后继节点,唤醒该节点关联的线程
    if (s != null) {
        LockSupport.unpark(s.thread);
    }
}
  • 唤醒的是"有效后继节点":如果head的直接后继节点是null或CANCELLED(无效节点),会从队列尾部向前遍历,找到最前面的一个有效节点(waitStatus ≤ 0),唤醒该节点------这样做是为了避免唤醒无效节点,提高效率
  • 唤醒后线程的行为:被唤醒的线程会从acquireQueued方法的自旋逻辑中继续执行,再次尝试tryAcquire获取资源,如果获取成功,就成为新的head节点;如果失败,继续阻塞等待

2、共享式模式

共享式模式适用于"多个线程可以同时获取资源"的场景,比如Semaphore(信号量)、CountDownLatch(倒计时器)、ReadWriteLock的读锁(多个线程可同时读)。

  • 共享式模式的核心流程也分为"获取资源(acquireShared)"和"释放资源(releaseShared)"两部分,与独占式模式的区别在于
    • 获取资源时,多个线程可以同时成功(比如Semaphore的state=3,可同时有3个线程获取资源)
    • 释放资源时,需要唤醒后续所有等待的有效节点(传播特性),而不是只唤醒一个后继节点

2.1、共享式获取资源:acquireShared(int arg)流程

acquireShared是AQS的模板方法,用于获取共享资源,arg是获取资源所需的数量(比如Semaphore中arg=1,表示获取1个许可),源码如下:

java 复制代码
public final void acquireShared(int arg) {
    // 步骤1:尝试获取共享资源(tryAcquireShared由子类重写)
    // 返回值 ≥0:获取成功,返回剩余可用资源数量
    // 返回值 <0:获取失败,需要加入队列等待
    if (tryAcquireShared(arg) < 0) {
        // 步骤2:获取失败 → 加入队列,阻塞等待(doAcquireShared)
        doAcquireShared(arg);
    }
}
  • 关键区别:tryAcquireShared方法的返回值是int类型,而不是boolean类型
    • 返回值 ≥0:获取资源成功,返回值表示"剩余的可用资源数量"
    • 返回值 <0:获取资源失败,返回值的绝对值表示"当前线程需要等待的条件"(比如-1表示需要等待资源可用)
步骤1:tryAcquireShared(int arg) ------ 子类自定义共享获取逻辑

tryAcquireShared是AQS的抽象方法,由子类重写,AQS默认实现抛出异常,源码如下:

java 复制代码
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

Semaphore的tryAcquireShared实现为例(非公平模式):

java 复制代码
// Semaphore的非公平同步器实现(NonfairSync)
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        // 1. 获取当前可用许可数量(state)
        int available = getState();
        // 2. 计算获取acquires个许可后,剩余的许可数量
        int remaining = available - acquires;
        // 3. 如果剩余许可 <0,或CAS修改state成功 → 返回剩余许可数量
        if (remaining < 0 ||
            compareAndSetState(available, remaining)) {
            return remaining;
        }
    }
}
  • 自旋CAS修改state:多个线程同时尝试获取许可时,通过自旋CAS修改state,保证原子性
  • 返回值含义:如果remaining ≥0,表示获取成功,返回剩余许可数量;如果remaining <0,表示获取失败,返回负数,线程会加入队列等待
步骤2:doAcquireShared(int arg) ------ 加入队列,阻塞等待

如果tryAcquireShared返回负数(获取失败),线程会被封装成共享模式的Node节点,加入队列,阻塞等待,直到被唤醒后重新尝试获取资源。该方法与独占式的acquireQueued方法类似,但有一个关键区别:共享式获取成功后,会唤醒后续的共享节点(传播特性),源码简化如下:

java 复制代码
private void doAcquireShared(int arg) {
    // 1. 创建共享模式的Node节点,加入队列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 2. 前驱是head,尝试获取共享资源
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 3. 获取成功 → 将当前节点设为新的head,并唤醒后续共享节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // 帮助GC
                    if (interrupted) {
                        selfInterrupt();
                    }
                    failed = false;
                    return;
                }
            }
            // 4. 获取失败 → 阻塞当前线程(逻辑和独占式一致)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) {
                interrupted = true;
            }
        }
    } finally {
        if (failed) {
            cancelAcquire(node);
        }
    }
}

关键细节:setHeadAndPropagate(node, r)方法------不仅会将当前节点设为新的head,还会根据剩余资源数量(r),判断是否需要唤醒后续的共享节点(传播特性),源码核心逻辑如下:

java 复制代码
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // 保存原head节点
    setHead(node); // 将当前节点设为新的head
    // 如果剩余资源>0,或原head为null、原head状态<0 → 唤醒后续共享节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 如果后继节点是共享模式,唤醒该节点
        if (s == null || s.isShared()) {
            doReleaseShared();
        }
    }
}

传播特性的作用:比如Semaphore的state=3,有5个线程同时获取资源,前3个线程获取成功(state=0),第4、5个线程加入队列等待;当其中一个线程释放资源(state=1),会唤醒第4个线程,第4个线程获取成功(state=0)后,会通过传播特性,继续唤醒第5个线程(即使state=0,也会尝试唤醒,让第5个线程检查是否能获取资源)。

2.2、共享式释放资源:releaseShared(int arg)流程

共享式释放资源的核心是"释放资源后,唤醒所有后续等待的共享节点",AQS的模板方法releaseShared源码如下:

java 复制代码
public final boolean releaseShared(int arg) {
    // 步骤1:尝试释放共享资源(tryReleaseShared由子类重写)
    if (tryReleaseShared(arg)) {
        // 步骤2:释放成功 → 唤醒后续所有等待的共享节点
        doReleaseShared();
        return true;
    }
    return false;
}
步骤1:tryReleaseShared(int arg) ------ 子类自定义共享释放逻辑

该方法由子类重写,核心作用是"释放共享资源,修改state状态",返回true表示释放成功(需要唤醒后续节点),返回false表示释放失败。以Semaphore的tryReleaseShared实现为例:

java 复制代码
// Semaphore的同步器实现(FairSync和NonfairSync共用)
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        // 1. 获取当前可用许可数量(state)
        int current = getState();
        // 2. 计算释放releases个许可后,新的许可数量
        int next = current + releases;
        // 3. 防止释放过多(比如释放的数量超过获取的数量)
        if (next < current) {
            throw new Error("Maximum permit count exceeded");
        }
        // 4. CAS修改state,成功则返回true,失败则自旋重试
        if (compareAndSetState(current, next)) {
            return true;
        }
    }
}

逻辑解析:通过自旋CAS修改state,保证多线程释放资源的原子性;释放成功后返回true,触发后续的唤醒逻辑。

步骤2:doReleaseShared() ------ 唤醒后续所有等待的共享节点
java 复制代码
private void doReleaseShared() {
    // 自旋(死循环),直到满足退出条件
    for (;;) {
        // 保存当前的head节点(AQS队列的头节点)
        Node h = head;
        // 条件1:head不为null(队列初始化完成)
        // 条件2:head != tail(队列中有等待的节点,不是空队列)
        if (h != null && h != tail) {
            // 获取头节点的等待状态
            int ws = h.waitStatus;
            
            // 情况1:头节点状态是SIGNAL → 说明有后继节点需要被唤醒
            if (ws == Node.SIGNAL) {
                // CAS尝试将头节点的状态从SIGNAL改为0
                // CAS失败的原因:可能有其他线程同时在修改head的状态,需要自旋重试
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    continue; // CAS失败,重新进入循环
                }
                // CAS成功,唤醒头节点的后继节点(和独占式唤醒逻辑一致)
                unparkSuccessor(h);
            }
            // 情况2:头节点状态是0 → 需要设置为PROPAGATE,保证共享模式的传播性
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                // CAS失败(其他线程修改了状态),自旋重试
                continue;
            }
        }
        // 退出条件:当前记录的head和实际的head一致(说明head没有被其他线程修改)
        // 如果head被修改(比如被唤醒的节点更新了head),则继续自旋
        if (h == head) {
            break;
        }
    }
}
相关推荐
m0_6070766010 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
NEXT0611 小时前
二叉搜索树(BST)
前端·数据结构·面试
NEXT0611 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
夏鹏今天学习了吗12 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
愚者游世13 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio
元亓亓亓14 小时前
LeetCode热题100--42. 接雨水--困难
算法·leetcode·职场和发展
源代码•宸15 小时前
Leetcode—200. 岛屿数量【中等】
经验分享·后端·算法·leetcode·面试·golang·dfs
用户1252055970815 小时前
后端Python+Django面试题
后端·面试
Tracy老板翻译官15 小时前
【团队管理问题篇】别让“凉粉冤案”毁了你的团队
网络·职场和发展·团队开发·创业创新·职场晋升
boooooooom16 小时前
Vue v-for + key 优化封神:吃透就地复用与强制重排,再也不卡帧!
javascript·vue.js·面试