Java面试系列文章
目录
- 引言
- 一、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多线程并发编程中,我们经常会用到ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier等同步工具,这些工具看似功能各异、互不相关,但它们的底层都依赖于同一个核心框架------AbstractQueuedSynchronizer(简称AQS)。
一、AQS核心定位与设计思想
1 、什么是AQS?
** AQS是Java并发包(java.util.concurrent.locks)中的一个抽象类,它并非一个具体的同步器,而是一个"同步器骨架"------它封装了同步状态的管理、线程的排队等待、唤醒等核心共性逻辑,开发者只需重写少量自定义方法,就能快速实现一个符合自身需求的同步器(比如ReentrantLock就是AQS的子类实现)。**
简单来说,AQS的作用就是"统一解决多线程竞争共享资源时的排队、唤醒问题",避免每个同步工具都重复实现一套排队逻辑,极大简化了同步器的开发难度。
2、核心设计思想:状态控制 + 队列管理
AQS的设计精髓可以概括为一句话:通过一个共享状态变量(state)控制资源的访问权限,通过一个双向阻塞队列(CLH队列)管理竞争失败的线程。这种设计将"同步的共性逻辑"(排队、唤醒、状态原子操作)与"同步的个性逻辑"(是否允许获取资源、如何释放资源)分离,采用模板方法模式实现。
- AQS抽象类:实现模板方法(如
acquire、release),封装排队、唤醒、状态原子操作等共性逻辑,不需要子类重写 - 子类(如ReentrantLock):重写AQS的抽象方法(如
tryAcquire、tryRelease),定义"如何获取资源、如何释放资源"的个性逻辑,也就是自定义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),直接决定了节点的行为(是否需要被唤醒、是否要被移除队列)CANCELLED(1):取消状态。当线程等待超时、被中断,或者主动放弃等待时,节点会被标记为CANCELLED。处于该状态的节点,会被AQS从队列中移除,不再参与资源竞争,也不会被唤醒SIGNAL(-1):信号状态。表示当前节点的后继节点正在等待被唤醒,当前节点释放资源(或被取消)时,必须唤醒它的后继节点。这是最常用的状态,也是保证队列唤醒逻辑的核心------当一个节点加入队列尾部时,会将其前驱节点的状态设置为SIGNAL,确保前驱释放资源时能唤醒自己CONDITION(-2):条件等待状态。该状态仅用于"条件队列"(由Condition接口维护,后续讲解),表示节点正在等待某个条件(如await()方法),直到其他线程调用signal()方法,节点才会从条件队列转移到同步队列,参与资源竞争PROPAGATE(-3):传播状态。仅用于共享模式(如Semaphore、CountDownLatch),表示当前节点获取资源成功后,需要将"资源可用"的信号传播给后续节点,让后续节点也能尝试获取资源(比如CountDownLatch中,state为0时,所有等待节点都会被唤醒,就是利用了传播特性)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;
- head节点是"当前持有资源的线程节点"(空节点,不关联线程),仅作为队列的起始标记
- 新加入的线程会追加到 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;
}
}
}