在 Java 并发编程领域,java.util.concurrent(JUC)包是我们实现高并发、线程安全的核心工具,而AQS(AbstractQueuedSynchronizer,抽象队列同步器) 就是撑起整个 JUC 包的底层骨架。无论是ReentrantLock、ReentrantReadWriteLock,还是Semaphore、CountDownLatch等并发工具,底层都依赖 AQS 实现。
本文将从AQS 核心原理 、ReentrantLock 基于 AQS 的实现 、自旋锁三个维度,带你彻底吃透 AQS,轻松应对面试与实际开发。
一、什么是 AQS?
AQS 是java.util.concurrent.locks包下的抽象类 ,它封装了一套通用的线程同步机制,核心解决三个问题:
- 管理同步状态(资源的占用 / 释放标记)
- 阻塞 / 唤醒等待的线程
- 维护线程等待队列
1.1 AQS 的两大核心组件
AQS 的底层实现非常简洁,仅靠一个状态变量 + 一个同步队列完成所有同步逻辑:
(1)state:同步状态标记
AQS 用一个volatile修饰的int类型变量state表示资源状态,通过 CAS 原子操作修改它,这是线程安全的关键:
state=0:资源未被占用(锁空闲)state>0:资源已被占用(锁被持有,重入时 state 递增)
不同同步工具对state的定义不同:
ReentrantLock:state=0无锁,state=1独占锁,state>1重入次数CountDownLatch:state=N计数器,state=0放行所有线程
(2)CLH 同步队列
AQS 采用CLH(Craig.Landin.Hagersten)变种双向队列 存储被阻塞的线程,是FIFO(先进先出) 结构:
- 双向队列:支持队列头尾快速插入、删除节点
- FIFO 特性:保证等待最久的线程优先获取资源(公平锁核心)
- 节点:每个节点封装一个等待的线程,包含线程状态、前驱 / 后继节点引用
二、ReentrantLock:AQS 的经典实现
ReentrantLock(可重入锁)是 AQS 最典型的应用,它支持公平锁 和非公平锁两种模式,底层完全通过 AQS 实现加锁、解锁、重入逻辑。
2.1 公平锁 vs 非公平锁
这是面试高频考点,核心区别在于线程是否能插队获取锁:
- 公平锁:严格按照线程请求顺序分配锁,先到先得,不允许插队
- 非公平锁 :线程尝试获取锁时,若锁刚好释放,可直接插队获取;若锁被占用,再进入队列等待
非公平锁是
ReentrantLock的默认实现,因为插队能减少线程阻塞唤醒的开销,吞吐量更高。
2.2 ReentrantLock 的 AQS 结构
ReentrantLock内部定义了 3 个核心类,全部基于 AQS 扩展:
Sync:抽象同步器,继承 AQS,封装公共的加锁、解锁逻辑NonfairSync:非公平锁同步器,继承 SyncFairSync:公平锁同步器,继承 Sync
2.3 加锁源码解析(非公平锁)
非公平锁的加锁流程分为尝试快速获取锁 和阻塞获取锁两步:
- 线程调用
lock(),先通过 CAS 尝试将state从 0 改为 1,成功则直接持有锁 - 若失败,判断是否是当前线程重入(持有锁的线程再次加锁),
state递增,成功返回 - 若都失败,调用 AQS 的
acquire()方法,将线程加入 CLH 队列阻塞
核心源码简化:
// 非公平锁初始尝试加锁
final boolean initialTryLock() {
Thread current = Thread.currentThread();
// CAS修改state=1,成功则持有锁
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
// 可重入:当前线程已持有锁,state+1
else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
setState(c);
return true;
}
return false;
}
2.4 加锁源码解析(公平锁)
公平锁比非公平锁多一步队列校验 :必须先判断队列中是否有等待线程,没有才能尝试加锁,绝对禁止插队。
核心源码简化:
// 公平锁初始尝试加锁
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键:先检查队列无等待线程,再CAS加锁
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑同上
else if (getExclusiveOwnerThread() == current) {
setState(++c);
return true;
}
return false;
}
2.5 解锁源码解析
解锁逻辑是加锁的逆过程,公平锁和非公平锁完全通用:
- 线程调用
unlock(),state递减 1 - 若
state=0,表示锁完全释放,清空持有线程 - 唤醒 CLH 队列中第一个等待的线程
核心源码简化:
// 尝试释放锁
protected final boolean tryRelease(int releases) {
// state-1
int c = getState() - releases;
// 非持有锁线程解锁,抛异常
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
// state=0,锁完全释放
boolean free = (c == 0);
if (free)
setExclusiveOwnerThread(null);
setState(c);
return free;
}
// 释放成功,唤醒队列下一个线程
public final boolean release(int arg) {
if (tryRelease(arg)) {
signalNext(head); // 唤醒头节点的后继线程
return true;
}
return false;
}
三、自旋锁:AQS 中的高效等待机制
在 AQS 和 CAS 操作中,自旋锁是提升性能的关键设计,我们先搞懂它的核心逻辑。
3.1 什么是自旋锁?
当线程获取锁失败时,不进入阻塞状态 ,而是通过无限循环(自旋) 不断尝试获取锁,直到成功为止。
简单来说:线程不休息,一直循环 "问" 锁是否释放。
自旋锁伪代码:
// 自旋锁核心逻辑
while(true){
if(尝试获取锁成功){
break;
}
// 否则继续循环
}
3.2 自旋锁在 AQS 中的应用
ReentrantLock:加锁失败后,先自旋尝试 CAS 修改state,多次失败后再入队列阻塞- 原子类(AtomicInteger):CAS 失败后,自旋重试,直到修改成功
3.3 自旋锁的优缺点
优点
- 轻量高效:避免线程阻塞 / 唤醒的内核态切换开销(用户态内完成等待)
- 适合锁竞争不激烈、锁持有时间短的场景,性能远超阻塞锁
缺点
- 占用 CPU:自旋时 CPU 一直做无用功,竞争激烈时会浪费大量 CPU 资源
- 不适合锁持有时间长的场景,会导致系统负载飙升
四、面试核心总结(必背)
-
AQS 是什么? 抽象队列同步器,JUC 核心,用
state状态 + CLH 双向队列实现线程同步。 -
AQS 核心原理?
- 用
volatile state标记资源状态,CAS 原子修改 - CLH FIFO 队列存储阻塞线程
- 加锁失败入队列阻塞,解锁唤醒队列首线程
- 用
-
公平锁 vs 非公平锁?
- 公平锁:排队获取,不插队,线程顺序执行
- 非公平锁:允许锁释放时插队,吞吐量更高,是默认实现
-
可重入锁原理? 持有锁的线程再次加锁,
state递增;解锁时state递减,直到state=0才完全释放锁。 -
自旋锁优缺点?
- 优点:避免线程阻塞切换,短锁场景性能高
- 缺点:长锁 / 高竞争场景浪费 CPU
五、总结
AQS 是 Java 并发编程的底层灵魂 ,它把复杂的线程同步逻辑封装成通用框架,让我们无需手动处理线程阻塞、唤醒、队列管理,只需基于state实现自定义同步工具。
ReentrantLock作为 AQS 的经典实现,通过公平 / 非公平模式适配不同业务场景;自旋锁则是 AQS 性能优化的关键,在短锁场景下大幅提升并发效率。
理解 AQS,不仅能轻松应对并发面试,更能让你在开发中写出更高效、更稳定的并发代码。