在Java并发编程中,我们经常使用ReentrantLock、Semaphore、CountDownLatch等同步工具,却很少思考这些工具背后的"统一底层支撑"------它们本质上都是基于**AbstractQueuedSynchronizer(简称AQS)**实现的。AQS是Java并发包(java.util.concurrent.locks)的核心骨架,定义了一套通用的同步器框架,封装了线程排队、锁获取与释放、中断响应等核心逻辑,让开发者无需重复实现复杂的同步机制,就能快速构建安全高效的同步工具。
本文将从AQS的核心定位出发,逐步拆解其底层结构、核心机制、两种工作模式,结合源码片段和实战案例,详细讲解AQS的工作原理,让你不仅能"知其然",更能"知其所以然",彻底搞懂Java并发同步的底层逻辑。
一、先搞懂:AQS是什么,为什么需要它?
1. AQS的核心定位
AQS的全称是AbstractQueuedSynchronizer,翻译为"抽象队列同步器",它是一个抽象类,本身不直接实现同步功能,而是提供了一套同步器的"模板骨架"------定义了线程竞争资源的核心流程(排队、阻塞、唤醒),将具体的"资源获取/释放逻辑"交给子类去实现。
简单来说,AQS的核心作用是:统一管理线程的排队和调度,封装通用同步逻辑,简化同步工具的开发。如果没有AQS,ReentrantLock、Semaphore等工具都需要各自实现线程排队、阻塞唤醒等逻辑,会导致代码冗余、效率低下,且难以保证线程安全。
2. 为什么需要AQS?
在并发编程中,线程之间的竞争与协作是核心问题,而同步工具的核心需求的是"控制线程对资源的访问权限"。如果没有统一的框架,开发者实现同步逻辑时会面临两个核心难题:
-
线程竞争无序:多个线程同时竞争资源时,容易出现"插队"现象,导致公平性无法保证,甚至出现死锁;
-
线程调度低效:线程获取资源失败后,若一直自旋重试,会浪费大量CPU资源;若直接终止,又无法保证后续能重新获取资源。
AQS通过"状态管理+队列调度"的组合,完美解决了这两个问题:用一个volatile状态变量控制资源访问权限,用一个FIFO双向队列管理等待线程,实现了线程的有序排队和高效调度,同时支持公平/非公平两种模式,兼顾性能与公平性。
3. AQS的核心设计思想
AQS的设计遵循"模板方法模式",核心思想可以概括为3点,也是AQS的三大核心组成部分:
-
一个volatile状态变量(state):用于表示资源的占用状态,子类通过操作该状态实现资源的获取与释放;
-
一个FIFO双向等待队列:用于存储获取资源失败的线程,线程进入队列后会阻塞,等待被唤醒后重新竞争资源;
-
一套模板方法+钩子方法:AQS定义了获取/释放资源的核心流程(模板方法),将具体的资源操作逻辑(钩子方法)交给子类实现,无需子类关心排队和调度细节。
一句话总结:AQS做"通用的事"(排队、阻塞、唤醒),子类做"具体的事"(资源的获取与释放),分工明确,灵活高效。
二、AQS底层结构拆解:状态+队列,缺一不可
AQS的底层核心是"状态变量state"和"双向等待队列",两者协同工作,实现线程的同步与调度。下面我们逐一拆解这两个核心组件,结合源码片段,搞懂其底层实现。
1. 核心状态变量:volatile int state
(1)state的作用
state是AQS中最核心的成员变量,用volatile修饰,保证了线程之间的可见性,用于表示资源的占用状态,其具体含义由子类决定(AQS不固定state的含义)。常见的state含义:
-
ReentrantLock中:state表示"锁的重入次数",0表示锁未被占用,大于0表示锁被占用(state的值等于重入次数);
-
Semaphore中:state表示"可用资源的数量",0表示无可用资源,线程需排队等待;
-
CountDownLatch中:state表示"倒计时计数器",0表示倒计时结束,所有等待线程被唤醒。
(2)state的核心操作方法
AQS提供了3个核心方法操作state,均基于CAS实现,保证操作的原子性,避免线程安全问题,子类可直接调用:
-
getState():获取当前state的值,简单的读操作,因为state是volatile的,无需加锁; -
setState(int newState):设置state的值,仅在子类确定当前线程拥有操作权限时使用(如释放锁时); -
compareAndSetState(int expect, int update):CAS操作,仅当当前state的值等于expect时,将其更新为update,返回true;否则返回false,是实现线程安全竞争的核心方法。
示例源码(AQS中state相关定义):
java
// 核心状态变量,volatile保证可见性
private volatile int state;
// 获取当前状态
protected final int getState() {
return state;
}
// 设置状态(无CAS,需子类保证线程安全)
protected final void setState(int newState) {
state = newState;
}
// CAS更新状态,原子操作
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
注意:AQS本身不限制state的取值范围和含义,完全由子类根据自身需求定义,这也是AQS灵活性的核心体现。
2. 双向等待队列:CLH变体队列
当线程通过CAS获取state失败(即资源被其他线程占用)时,不会一直自旋重试,而是会被封装成一个"节点(Node)",加入到AQS的双向等待队列中,然后阻塞自己,等待被前驱节点唤醒------这个队列是AQS实现线程排队的核心,本质是一个CLH锁队列的变体(CLH是一种基于链表的无锁队列,用于实现公平锁)。
(1)队列的结构
AQS的等待队列是一个FIFO(先进先出)双向链表,每个节点代表一个等待中的线程,队列有两个核心指针:head(头节点)和tail(尾节点),初始时head和tail都为null,当有线程加入队列时,tail指针不断后移,head指针仅在有线程释放资源、唤醒后继线程时移动。
队列结构示意图(简化):
head(哨兵节点) ←→ Node1(线程1) ←→ Node2(线程2) ←→ ... ←→ tail(尾节点)
说明:head节点是一个"哨兵节点"(空节点),不对应任何等待线程,其作用是简化队列的操作(避免判断head是否为null),真正的等待线程从head的后继节点开始。
(2)Node节点的核心属性
每个Node节点封装了等待线程的相关信息,核心属性如下(源码简化版):
java
static final class Node {
// 标记节点的等待状态(核心属性)
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 当前节点对应的线程
volatile Thread thread;
// 条件队列相关(后续讲解)
Node nextWaiter;
// 等待状态的常量值
static final int CANCELLED = 1; // 线程被取消(如超时、中断)
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 线程在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下,状态需要传播
}
其中,waitStatus是Node节点的核心属性,用于标记当前线程的等待状态,决定了线程的行为(是否需要阻塞、是否能被唤醒),各状态的含义如下:
-
CANCELLED(1):线程因超时或被中断,已取消等待,该节点会被从队列中移除,不再参与资源竞争;
-
SIGNAL(-1):当前节点的后继节点正在阻塞,当前节点释放资源后,需要唤醒后继节点;
-
CONDITION(-2):线程在条件队列中等待(通过Condition.await()方法进入),需等待条件满足后被唤醒;
-
PROPAGATE(-3):仅用于共享模式,标识当前资源的释放状态需要向后传播,唤醒所有等待的线程;
-
0:默认状态,线程正常等待,未被取消、未被标记为唤醒后继节点。
(3)队列的核心操作:入队与出队
AQS封装了队列的入队(enq)和出队(deq)操作,均基于CAS实现,保证线程安全,子类无需关心具体实现,核心流程如下:
-
入队操作(enq):线程获取资源失败后,调用enq方法将节点加入队列尾部。
-
若队列为空(head和tail都为null),先创建一个哨兵节点作为head,再将当前节点作为tail;
-
若队列不为空,通过CAS将当前节点设置为新的tail,同时将原tail的next指向当前节点;
-
入队完成后,线程调用LockSupport.park()方法阻塞自己,等待被唤醒。
-
-
出队操作(deq):线程释放资源后,会唤醒head的后继节点,该节点获取资源成功后,将自己设置为新的head(原head节点被回收),完成出队。
-
出队仅发生在"线程获取资源成功"时,由获取资源成功的线程主动完成;
-
出队后,新的head节点仍为哨兵节点,保证队列结构的一致性。
-
核心总结:队列的入队和出队操作均为线程安全,通过CAS避免并发问题,FIFO的结构保证了线程排队的有序性,哨兵节点的设计简化了队列操作。
三、AQS核心机制:模板方法+两种工作模式
AQS的核心竞争力在于"模板方法模式"的设计,以及支持"独占模式"和"共享模式"两种工作模式,适配不同的同步场景(如独占锁、共享锁)。下面我们详细讲解这两个核心机制,结合源码理解AQS的工作流程。
1. 模板方法模式:AQS的"骨架"
AQS定义了一套"获取资源"和"释放资源"的模板方法,这些方法是final修饰的,子类无法重写,保证了核心流程的一致性;同时提供了一组"钩子方法"(protected修饰,空实现或抛出异常),子类需要根据自身需求重写这些钩子方法,实现具体的资源获取/释放逻辑。
(1)核心模板方法(获取资源)
AQS提供了两种获取资源的模板方法,对应两种工作模式,核心逻辑一致:先尝试获取资源,获取失败则入队阻塞,等待被唤醒后重新尝试。
- 独占模式获取资源 :
acquire(int arg),对应ReentrantLock等独占锁,同一时刻只有一个线程能获取资源。
java
// 独占模式获取资源,模板方法,final不可重写
public final void acquire(int arg) {
// 1. 尝试获取资源(钩子方法,子类实现)
// 2. 若获取失败,将当前线程封装为Node,加入队列
// 3. 阻塞当前线程,等待被唤醒后重新尝试获取
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 若线程被中断,补充中断响应
selfInterrupt();
}
}
- 共享模式获取资源 :
acquireShared(int arg),对应Semaphore、CountDownLatch等共享锁,同一时刻多个线程可获取资源。
java
// 共享模式获取资源,模板方法,final不可重写
public final void acquireShared(int arg) {
// 1. 尝试获取共享资源(钩子方法,子类实现)
// 2. 若获取失败,入队阻塞
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
(2)核心模板方法(释放资源)
释放资源的模板方法同样分为独占模式和共享模式,核心逻辑是:释放资源(更新state),然后唤醒队列中的后继线程,让其重新竞争资源。
- 独占模式释放资源:
java
release(int arg)public final boolean release(int arg) {
// 1. 尝试释放资源(钩子方法,子类实现)
if (tryRelease(arg)) {
// 2. 唤醒head的后继节点
Node h = head;
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h);
}
return true;
}
return false;
}
- 共享模式释放资源:
java
releaseShared(int arg)public final boolean releaseShared(int arg) {
// 1. 尝试释放共享资源(钩子方法,子类实现)
if (tryReleaseShared(arg)) {
// 2. 唤醒后继节点,共享模式可能需要唤醒多个线程
doReleaseShared();
return true;
}
return false;
}
(3)钩子方法(子类需重写)
AQS的钩子方法均为protected修饰,子类根据自身需求重写,未重写则抛出UnsupportedOperationException异常,核心钩子方法如下:
| 钩子方法 | 模式 | 作用 | 示例子类 |
|---|---|---|---|
| tryAcquire(int arg) | 独占模式 | 尝试获取独占资源,返回true表示获取成功,false表示失败 | ReentrantLock |
| tryRelease(int arg) | 独占模式 | 尝试释放独占资源,返回true表示释放成功,false表示失败 | ReentrantLock |
| tryAcquireShared(int arg) | 共享模式 | 尝试获取共享资源,返回>=0表示获取成功,<0表示失败 | Semaphore、CountDownLatch |
| tryReleaseShared(int arg) | 共享模式 | 尝试释放共享资源,返回true表示释放成功,false表示失败 | Semaphore、CountDownLatch |
| isHeldExclusively() | 独占模式 | 判断当前线程是否独占资源,用于Condition相关操作 | ReentrantLock |
核心总结:模板方法定义了"获取-排队-阻塞-唤醒-释放"的完整流程,钩子方法定义了"资源如何获取/释放",子类只需重写钩子方法,就能快速实现同步工具------这就是AQS的强大之处。
2. 两种工作模式:独占模式 vs 共享模式
AQS支持两种资源竞争模式,分别对应不同的同步场景,子类可根据需求选择实现其中一种模式,也可同时实现两种模式(如ReentrantReadWriteLock,读锁是共享模式,写锁是独占模式)。
(1)独占模式(Exclusive Mode)
核心特点:同一时刻,只有一个线程能获取资源,其他线程只能排队等待,直到持有资源的线程释放资源。
-
适用场景:独占锁(如ReentrantLock)、互斥同步(确保同一时刻只有一个线程执行临界区代码);
-
核心逻辑:线程获取资源时,需判断state是否为0(未被占用),若为0则通过CAS将state设为1(或重入次数),获取成功;若不为0,则入队阻塞;释放资源时,将state重置为0,唤醒后继线程。
-
补充:独占模式支持"重入"(如ReentrantLock),即持有资源的线程可再次获取资源,只需将state自增(重入次数+1),释放时自减,直到state为0时真正释放资源。
(2)共享模式(Shared Mode)
核心特点:同一时刻,多个线程可同时获取资源,资源的可用数量会随着线程的获取而减少,随着线程的释放而增加。
-
适用场景:共享锁(如Semaphore的许可、CountDownLatch的倒计时、ReentrantReadWriteLock的读锁);
-
核心逻辑:线程获取资源时,判断state是否大于0(有可用资源),若大于0则通过CAS减少state(可用资源-1),获取成功;若等于0,则入队阻塞;释放资源时,通过CAS增加state(可用资源+1),唤醒所有等待的线程(因为多个线程可同时获取资源)。
-
补充:共享模式下,线程获取资源后,不会阻止其他线程获取资源(只要有可用资源),适合"读多写少"的场景(如缓存读取)。
(3)两种模式对比
| 对比维度 | 独占模式 | 共享模式 |
|---|---|---|
| 资源占用 | 同一时刻仅一个线程占用 | 同一时刻多个线程占用 |
| state含义 | 锁的重入次数(0表示未占用) | 可用资源数量(0表示无可用) |
| 唤醒逻辑 | 释放资源时,仅唤醒一个后继线程 | 释放资源时,唤醒所有等待线程 |
| 代表子类 | ReentrantLock(写锁) | Semaphore、CountDownLatch、ReentrantReadWriteLock(读锁) |
四、实战解析:AQS在ReentrantLock中的应用
光懂理论不够,我们结合ReentrantLock(可重入独占锁)的源码,看看AQS的钩子方法是如何被重写的,以及AQS的核心流程如何工作------ReentrantLock是AQS独占模式的典型实现,也是我们日常开发中最常用的同步工具之一。
1. ReentrantLock的核心实现
ReentrantLock内部有一个静态内部类Sync,该类继承自AQS,重写了AQS的tryAcquire、tryRelease、isHeldExclusively三个钩子方法,实现了独占模式的资源获取与释放。ReentrantLock支持公平锁和非公平锁,两者的区别仅在于tryAcquire方法的实现不同。
(1)非公平锁的tryAcquire实现(默认)
非公平锁的特点:线程获取资源时,不会先判断队列是否有等待线程,而是直接尝试CAS获取资源,若获取成功则直接持有锁,效率更高(但可能导致线程饥饿)。
java
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 重写AQS的tryAcquire方法,尝试获取独占资源
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 1. 若state为0(锁未被占用),直接CAS获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 标记当前线程为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
// 2. 若当前线程已持有锁(重入),state自增
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // 溢出判断
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 3. 锁被其他线程持有,获取失败
return false;
}
}
(2)tryRelease方法的实现
释放锁时,减少state(重入次数),当state减为0时,标记锁的持有者为null,真正释放锁。
java
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 只有锁的持有者才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 当state减为0时,释放锁,标记持有者为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2. ReentrantLock的工作流程(结合AQS)
以非公平锁为例,ReentrantLock的lock()和unlock()方法的工作流程,本质就是AQS模板方法的执行流程:
-
线程调用lock()方法,底层调用AQS的acquire(1)模板方法;
-
acquire(1)调用ReentrantLock重写的tryAcquire(1)方法,尝试获取资源:
-
若state为0,CAS将state设为1,标记当前线程为持有者,获取成功,流程结束;
-
若当前线程已持有锁(重入),state自增1,获取成功,流程结束;
-
若锁被其他线程持有,tryAcquire返回false,进入下一步。
-
-
AQS将当前线程封装为Node节点,加入双向等待队列,调用LockSupport.park()阻塞线程;
-
持有锁的线程调用unlock()方法,底层调用AQS的release(1)模板方法;
-
release(1)调用ReentrantLock重写的tryRelease(1)方法,state减1:
-
若state减为0,释放锁(标记持有者为null),返回true;
-
AQS唤醒head的后继节点,被唤醒的线程重新尝试获取资源(重复步骤2)。
-
五、AQS进阶:条件变量(Condition)
除了核心的状态管理和队列调度,AQS还提供了条件变量(Condition)的支持,用于实现"线程等待-唤醒"的精细化控制------Condition类似于Object的wait()和notify()方法,但比其更灵活(支持多个条件队列)。
1. Condition的核心作用
Condition依托于AQS实现,每个Condition对应一个条件队列(单向链表),用于存储调用Condition.await()方法后阻塞的线程。当条件满足时,调用Condition.signal()或signalAll()方法,唤醒条件队列中的线程,让其重新竞争资源。
核心优势:一个锁可以对应多个Condition,实现不同条件下的线程等待与唤醒,例如:生产者-消费者模型中,可通过两个Condition分别管理"生产者等待队列"和"消费者等待队列",避免不必要的唤醒。
2. Condition的核心方法
-
await():线程调用该方法后,释放所持有的锁,进入当前Condition的条件队列,阻塞自己,等待被唤醒; -
signal():唤醒条件队列中的第一个线程,将其转移到AQS的等待队列中,让其重新竞争锁; -
signalAll():唤醒条件队列中的所有线程,将其全部转移到AQS的等待队列中。
3. Condition与AQS队列的关系
AQS有两个核心队列:同步队列 (双向链表,用于存储获取锁失败的线程)和条件队列(单向链表,用于存储Condition.await()阻塞的线程),两者的关系如下:
-
线程调用Condition.await()时,会先释放锁,然后从同步队列中移除,加入到条件队列中,阻塞自己;
-
线程调用Condition.signal()时,会将条件队列中的线程移出,加入到同步队列中,等待重新获取锁;
-
条件队列是依赖于同步队列存在的,只有持有锁的线程,才能调用Condition的await()和signal()方法(否则会抛出IllegalMonitorStateException异常)。
六、常见误区与注意事项
学习AQS时,很容易陷入一些误区,这里总结几个高频误区,帮助大家避坑:
-
误区1:AQS是一个锁------错误。AQS不是锁,而是一个同步器框架,是实现锁和同步工具的"底层骨架",ReentrantLock、Semaphore等才是锁/同步工具。
-
误区2:state的值只能是0或1------错误。state的含义由子类定义,可根据需求取任意整数(如ReentrantLock中state是重入次数,可大于1;Semaphore中state是可用资源数,可大于1)。
-
误区3:AQS的队列是单向链表------错误。AQS的等待队列是双向链表,双向链表的优势是便于节点的删除(如线程被取消时,可快速调整前驱和后继节点的指针)。
-
误区4:共享模式下,所有线程都能获取资源------错误。共享模式下,线程能否获取资源,取决于state的值(可用资源数),当state为0时,线程仍需排队等待。
-
注意事项:子类重写钩子方法时,必须保证线程安全(通常基于CAS操作);Condition的await()和signal()方法,必须在持有锁的情况下调用,否则会抛出异常。
七、总结:AQS的核心价值与应用场景
AQS作为Java并发包的核心骨架,其核心价值在于"封装通用同步逻辑,简化同步工具的开发"------它将线程排队、阻塞唤醒、状态管理等复杂逻辑封装起来,让开发者只需关注"资源的获取与释放",就能快速实现安全高效的同步工具。
AQS的核心本质可以概括为:一个volatile状态变量(state)+ 一个FIFO双向等待队列 + 一套模板方法 + 两种工作模式,四种组件协同工作,实现了线程的有序竞争与高效调度。
常见的基于AQS实现的同步工具:
-
独占模式:ReentrantLock(可重入独占锁)、ReentrantLock.WriteLock(写锁);
-
共享模式:Semaphore(信号量)、CountDownLatch(倒计时器)、CyclicBarrier(循环屏障)、ReentrantLock.ReadLock(读锁)。
掌握AQS,不仅能让你更好地理解Java并发工具的底层原理,在遇到并发问题时,还能基于AQS自定义同步工具,满足特定业务场景的需求------这也是Java并发编程从"会用"到"精通"的关键一步。