吃透AQS:JUC并发工具的底层骨架,这篇讲透所有原理!
在Java并发编程中,你一定用过ReentrantLock、CountDownLatch、Semaphore这些JUC工具——它们用法各异,却藏着同一个“底层密码”:AQS(AbstractQueuedSynchronizer,抽象队列同步器)。
就像盖房子无论户型如何,都离不开钢筋混凝土的地基,AQS就是所有JUC工具的“地基”,定义了线程竞争、排队、唤醒的核心规则。吃透AQS,你再看任何JUC工具都会豁然开朗,面试时遇到并发底层问题也能对答如流!
👉 还没搞懂synchronized底层的同学,先戳这里补基础:《深入拆解synchronized:从对象头到锁升级,这篇讲透底层逻辑》
一、先搞懂:为什么AQS是“并发工具的万能骨架”?
你有没有发现一个神奇现象:ReentrantLock是“锁”、CountDownLatch是“倒计时器”、Semaphore是“信号量”,功能完全不同,但底层代码都绕不开AQS?
看几个简化源码片段就懂了:
// ReentrantLock的lock(),底层调用AQS的acquire()
public void lock() {
sync.acquire(1);
}
// CountDownLatch的await(),底层调用AQS的acquireShared()
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// Semaphore的acquire(),底层调用AQS的acquireShared()
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
AQS的核心价值的是:封装并发编程的共性问题(线程怎么排队、竞争失败怎么等、释放资源怎么唤醒下一个),让上层工具只需实现“个性化逻辑”(比如“能不能获取资源”“怎么释放资源”)。
用“排队买票”的通俗场景理解AQS:
- 售票窗口 = 共享资源(需要竞争的锁/资源);
- 买票的人 = 线程;
- 排队的队伍 = AQS的同步队列(竞争失败的线程在这里等候);
- 售票规则 = AQS定义的核心流程(先到先得、没票排队、买完叫下一个)。
不管是“单窗口售票(独占锁,如ReentrantLock)”还是“多窗口售票(共享锁,如Semaphore)”,排队、叫号的规则都由AQS定好,上层工具只需告诉AQS“怎么判断能不能买票”(比如ReentrantLock判断“窗口空不空”,Semaphore判断“还有没有余票”)。
二、AQS核心原理:3个组件+1个设计模式
AQS的核心结构特别简单:状态变量state + 同步队列(CLH队列)+ 模板方法模式。我们逐个拆解,用最通俗的语言讲透:
1. 核心组件1:state——资源的“占用凭证”
state是AQS的核心变量(用volatile修饰,保证线程可见性),代表“共享资源的占用状态”。不同JUC工具对state的定义完全不同:
| JUC工具 | state的含义 | 核心操作 |
|---|---|---|
| ReentrantLock(独占锁) | state=0→无锁;state≥1→被占用(值=重入次数) | 加锁state++,解锁state-- |
| Semaphore(信号量) | state=可用资源数量 | 申请资源state--,释放资源state++ |
| CountDownLatch(倒计时器) | state=需要等待的任务数 | 任务完成state--,state=0唤醒所有等待线程 |
AQS提供了3个原子操作state的方法(保证线程安全):
// 获取当前state值
protected final int getState() { return state; }
// 直接设置state(无竞争场景用)
protected final void setState(int newState) { state = newState; }
// CAS原子更新state(竞争场景用)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
简单说:state就是AQS的“资源计数器”,上层工具通过修改state实现“加锁/释放锁”的核心逻辑。
2. 核心组件2:同步队列(CLH队列)——线程的“排队区”
当线程竞争资源失败时,不会直接阻塞,而是被AQS包装成一个“Node节点”,加入到同步队列的尾部(通过CAS保证入队安全)。这个队列是双向链表,每个Node包含3个关键信息:
- 等待的线程(thread);
- 前驱节点(prev)、后继节点(next)(维持链表结构);
- 等待状态(waitStatus)(比如“等待唤醒(SIGNAL)”“取消等待(CANCELLED)”)。
同步队列的核心规则:
- 线程竞争失败 → 封装成Node → CAS加入队尾;
- 持有资源的线程释放锁 → 唤醒队首节点对应的线程;
- 被唤醒的线程再次尝试竞争资源(CAS修改state),成功则出队执行,失败则继续等待。
3. 核心设计:模板方法模式——上层工具的“个性化接口”
AQS最聪明的设计就是“模板方法模式”:它定义了锁竞争、排队、唤醒的完整流程(模板方法),把“能不能获取资源”“释放资源后做什么”这些个性化逻辑,留给上层工具去实现。
AQS的核心模板方法(上层工具直接调用):
- 独占模式(比如ReentrantLock):
acquire(int arg)(申请资源)、release(int arg)(释放资源); - 共享模式(比如Semaphore):
acquireShared(int arg)(申请共享资源)、releaseShared(int arg)(释放共享资源)。
上层工具需要实现的“个性化方法”:
- 独占模式:
tryAcquire(int arg)(判断能否获取独占锁)、tryRelease(int arg)(释放独占锁); - 共享模式:
tryAcquireShared(int arg)(判断能否获取共享锁)、tryReleaseShared(int arg)(释放共享锁)。
用“做饭”理解模板方法:AQS定义了“买菜→洗菜→炒菜→装盘”的固定流程(模板方法),上层工具只需告诉AQS“买什么菜”“放多少盐”(个性化方法),不用重复写整个流程。
4. AQS完整工作流程(独占模式,通俗版)
暂时无法在豆包文档外展示此内容
一句话总结:线程先尝试抢锁,抢不到就排队,等前一个线程释放锁后被唤醒,再重新抢锁——排队、阻塞、唤醒的复杂流程全由AQS搞定,上层工具只需要实现“抢锁”和“放锁”的判断逻辑。
三、实战:用AQS定制自己的“独占锁”(企业级简化版)
理解了AQS原理,我们动手实现一个简单的不可重入独占锁(类似ReentrantLock核心逻辑),实战感受AQS的强大。
1. 需求定义
- 同一时间只有1个线程能获取锁;
- 竞争失败的线程排队等待;
- 支持解锁操作,解锁后唤醒下一个排队线程。
2. 代码实现(基于AQS)
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
// 自定义独占锁:基于AQS实现
public class MyExclusiveLock implements Lock {
// 核心:内部类Sync继承AQS,实现个性化方法
private final Sync sync = new Sync();
// AQS子类:实现独占模式的tryAcquire和tryRelease
private static class Sync extends AbstractQueuedSynchronizer {
// 1. 尝试获取独占锁(个性化逻辑)
@Override
protected boolean tryAcquire(int arg) {
// state=0表示无锁,用CAS将state改为1
if (compareAndSetState(0, 1)) {
// 记录当前持有锁的线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// 已有线程持有锁,获取失败
return false;
}
// 2. 尝试释放独占锁(个性化逻辑)
@Override
protected boolean tryRelease(int arg) {
// 只有锁的持有者能解锁
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException("不是锁持有者,无法解锁");
}
// 释放锁:清空持有线程,state设为0
setExclusiveOwnerThread(null);
setState(0); // 无需CAS,因为只有持有者能执行
return true;
}
// 判断当前线程是否持有锁
protected boolean isHeldExclusively() {
return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
}
}
// ------------------- 实现Lock接口(调用AQS模板方法)-------------------
@Override
public void lock() {
sync.acquire(1); // 调用AQS的独占模式申请锁
}
@Override
public void unlock() {
sync.release(1); // 调用AQS的独占模式释放锁
}
// 其他Lock方法(按需实现)
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long time, java.util.concurrent.TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public java.util.concurrent.locks.Condition newCondition() {
return sync.newCondition();
}
}
3. 测试验证(高并发场景)
// 测试自定义锁的线程安全
public class MyLockTest {
private static int count = 0;
private static final MyExclusiveLock lock = new MyExclusiveLock();
public static void main(String[] args) throws InterruptedException {
// 10个线程,每个累加1000次
int threadNum = 10;
Thread[] threads = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(() -> {
lock.lock(); // 加锁
try {
for (int j = 0; j < 1000; j++) {
count++;
}
} finally {
lock.unlock(); // 解锁(必须在finally中)
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
System.out.println("最终计数:" + count); // 预期结果:10000
}
}
运行结果:最终计数=10000,说明自定义锁完全线程安全!这个案例完美体现AQS的价值——我们只写了“抢锁”和“放锁”的简单逻辑,线程排队、阻塞、唤醒的复杂流程全由AQS搞定。
四、面试必背:3道AQS高频题+标准答案
AQS是并发面试的“硬核考点”,掌握下面的答题模板,轻松应对面试官追问:
1. 面试题1:AQS的核心设计思想和核心组件是什么?
标准答案:
-
核心设计思想:模板方法模式+队列同步,封装线程排队、阻塞、唤醒的共性问题,暴露个性化接口让上层工具实现,降低并发工具开发难度;
-
核心组件:
- 状态变量state:volatile修饰,代表共享资源的占用状态(不同工具定义不同);
- 同步队列(CLH队列):双向链表,存储竞争失败的线程节点,支持CAS安全入队/出队;
- 独占/共享模式:适配不同场景(独占模式如ReentrantLock,共享模式如Semaphore)。
2. 面试题2:AQS的独占模式和共享模式有什么区别?对应哪些JUC工具?
标准答案:
-
核心区别:资源占用方式不同;
-
独占模式:同一时间只有1个线程能获取资源(单窗口售票);
- 核心方法:
acquire()、release(); - 对应工具:ReentrantLock、ReentrantReadWriteLock的写锁;
- 核心方法:
-
共享模式:同一时间多个线程可获取资源(多窗口售票),资源数量由state控制;
- 核心方法:
acquireShared()、releaseShared(); - 对应工具:Semaphore、CountDownLatch、ReentrantReadWriteLock的读锁。
- 核心方法:
3. 面试题3:ReentrantLock是如何基于AQS实现独占锁的?
标准答案:
ReentrantLock通过内部类Sync(继承AQS)实现,核心3步:
-
定义state含义:state=0→无锁,state≥1→线程持有锁(值=重入次数);
-
实现AQS个性化方法:
tryAcquire():CAS尝试将state从0→1(首次获取)或state+1(重入),记录持有线程;tryRelease():state-1,当state=0时清空持有线程,释放锁;
-
调用AQS模板方法:
lock()调用acquire()(抢锁失败排队),unlock()调用release()(释放锁唤醒下一个线程)。
五、明日预告
今天我们用“排队买票”的比喻吃透了AQS的核心:state状态变量+同步队列+模板方法模式。记住一句话:AQS不直接实现锁,而是给锁提供“排队、唤醒”的骨架,上层工具只需填充“抢锁”和“放锁”的逻辑。
👉 粉丝福利:为了帮大家梳理AQS核心知识点,我整理了《AQS原理思维导图》《JUC工具底层关系图》《并发面试高频题手册》,关注GZH【咖啡Java研习班】,回复“学习资料”即可免费领取!
👉 互动讨论:你在项目中用过哪些基于AQS的JUC工具?面试时被问过AQS的哪些问题?欢迎在评论区留言,我会逐一回复并补充到面试题库中!
明天我们将进入AQS实战升级篇——线程池的核心原理与企业级调优!线程池是高并发场景的“性能命脉”,也是面试必问重点:“为什么不建议用Executors创建线程池?”“IO密集型和CPU密集型场景如何调参?”“线程池的拒绝策略怎么选?”,明天我们一一拆解,带你从原理到实战,搞定线程池所有考点!
关注GZH 【咖啡 Java 研习班】,每天1篇Java并发干货,从基础到进阶,帮你轻松搞定多线程面试,成为资深Java工程师!