《Java 100 天进阶之路》第54篇:AQS抽象队列同步器(2026版)

第54篇:AQS抽象队列同步器(2026版)

📌 系列导航《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第53篇:volatile与JMM |
➡️ 下一篇:第55篇:线程池ThreadPoolExecutor

🗺️ 本文阅读地图(3 分钟速览)

第53篇搞懂了 JMM 和 volatile,本篇深入 JUC 并发体系的基石——AQS。掌握了 AQS,ReentrantLock、Semaphore、CountDownLatch 就全部打通了:

模块核心问题一句话回答
AQS 是什么为什么说它是 JUC 的基石?JUC 中所有锁和同步器(ReentrantLock、Semaphore、CountDownLatch 等)底层都基于 AQS
核心三要素AQS 由什么构成?volatile int state(同步状态)+ CLH 双向队列(等待队列)+ 模板方法模式(控制框架)
独占模式ReentrantLock 怎么实现?state 表示锁被占用次数,0=空闲,>0=被持有或重入
共享模式Semaphore/CountDownLatch 怎么实现?state 表示剩余许可数/剩余计数,多个线程可同时获取
面试最爱问高频考点有哪些?见文末 面试 小节


一、核心知识点

AQS 是什么?

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 java.util.concurrent 包(JUC)的底层基石——它是一个抽象类,为构建锁和同步器提供了通用框架

核心设计思想

AQS 的目标不是“提供一把具体的锁”,而是提供一个可复用的同步器框架——把“排队、阻塞、唤醒”这套通用流程封装好,子类只需实现“能不能获取同步状态”这个核心判断逻辑。

三大核心要素

  1. volatile int state:同步状态,用 CAS 原子更新
  2. CLH 双向队列(FIFO):存储等待获取同步状态的线程
  3. 模板方法模式:定义获取/释放同步状态的骨架,子类只需实现 tryAcquire/tryRelease 等方法

两种同步模式

模式特点典型实现
独占模式(Exclusive)同一时刻只有一个线程能获取同步状态ReentrantLock
共享模式(Shared)同一时刻多个线程可以同时获取同步状态Semaphore、CountDownLatch、ReentrantReadWriteLock(读锁)

二、生活类比:从“餐厅排队叫号”到“VIP 通道”

在这里插入图片描述

AQS 就像一个餐厅的“排队叫号系统”

  • state(同步状态):餐厅的空桌数量——独占模式下只有 1 张桌子(0=全满,1=空闲);共享模式下有多张桌子(state=剩余空桌数)。
  • CLH 队列:门口排队等位的顾客队伍(FIFO,先来后到)。
  • 模板方法:餐厅统一规定“排队、叫号、入座”的流程,但每个分店可以决定“什么情况下叫号”(子类实现 tryAcquire 逻辑)。

独占模式(ReentrantLock)就像“VIP 包间”

  • 包间只有 1 间,一次只能进 1 个人(state=0/1)。
  • 同一个 VIP 客人可以多次进入(可重入,state 累加)。
  • 其他客人在门口排队,包间客人出来后才让下一个进。

共享模式(Semaphore)就像“公共停车场”

  • 停车场有 N 个车位(state=N)。
  • 每来一辆车占一个车位(state-1),车位满了就排队。
  • 每走一辆车释放一个车位(state+1),唤醒排队车辆。
  • 多辆车可以同时进出,互不影响。

三、AQS 核心三要素

3.1 state:同步状态
// AQS 核心属性
private volatile int state;  // 同步状态,volatile 保证可见性

state 是 AQS 的“大脑”——所有同步逻辑都围绕它展开:

场景state 含义
独占模式(ReentrantLock)0=锁空闲,1=锁被占用,>1=同一线程重入次数
共享模式(Semaphore)剩余许可数量
共享模式(CountDownLatch)剩余计数(未完成线程数)
3.2 CLH 队列:等待线程的“排队系统”

AQS 内部维护一个 CLH 双向队列(Craig, Landin, and Hagersten 队列),用于管理等待获取同步状态的线程。

队列结构

在这里插入图片描述

Node 的状态(waitStatus)

状态含义
CANCELLED1线程已取消等待(超时或被中断)
SIGNAL-1后继线程需要被唤醒(当前节点释放锁时要唤醒下一个)
CONDITION-2线程在 Condition 上等待
PROPAGATE-3共享模式下需要向后传播(释放共享锁时通知所有后继)
00初始状态

入队流程

  1. 线程获取同步状态失败
  2. 包装成 Node 节点
  3. CAS 设置到队尾
  4. 进入自旋或阻塞等待
3.3 模板方法模式

AQS 使用模板方法模式,定义了获取/释放同步状态的骨架,子类只需实现特定方法:

子类需实现的方法说明
tryAcquire(int arg)独占式尝试获取同步状态
tryRelease(int arg)独占式尝试释放同步状态
tryAcquireShared(int arg)共享式尝试获取同步状态
tryReleaseShared(int arg)共享式尝试释放同步状态
isHeldExclusively()当前线程是否独占持有同步状态

AQS 提供的模板方法(子类直接调用):

  • acquire(int arg):独占式获取锁(模板方法)
  • release(int arg):独占式释放锁(模板方法)
  • acquireShared(int arg):共享式获取锁(模板方法)
  • releaseShared(int arg):共享式释放锁(模板方法)

在这里插入图片描述


四、独占模式:以 ReentrantLock 为例

独占模式下,同一时刻只有一个线程能持有同步状态。

4.1 加锁流程(acquire)
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&      // ① 子类实现:尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // ② 获取失败→入队
        selfInterrupt();          // ③ 中断当前线程
}

完整流程(以非公平锁为例)

在这里插入图片描述

4.2 解锁流程(release)
public final boolean release(int arg) {
    if (tryRelease(arg)) {       // ① 子类实现:尝试释放锁
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);   // ② 唤醒后继线程
        return true;
    }
    return false;
}
4.3 ReentrantLock 如何实现可重入?
// ReentrantLock 的 Sync 子类(非公平锁)的 tryAcquire 实现(简化)
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 锁空闲 → CAS 抢锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        // 当前线程已持有锁 → 重入!state++
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;  // 其他线程持有锁 → 获取失败
}

可重入的关键:判断当前线程是否已经是锁的持有者——如果是,直接 state++,无需再次竞争。


五、共享模式:以 Semaphore 和 CountDownLatch 为例

共享模式下,多个线程可以同时持有同步状态,state 的值代表剩余可用资源数。

5.1 Semaphore(信号量)——控制并发数

Semaphore 的 state 代表剩余许可数量

  • acquire()state - 1,如果 state > 0 则成功,否则入队等待
  • release()state + 1,唤醒等待线程

适用场景:限流——同一时刻最多 N 个线程访问某资源。

5.2 CountDownLatch(门栓)——等待所有线程完成

CountDownLatch 的 state 代表剩余计数(需要等待完成的线程数):

  • countDown()state - 1,当 state == 0 时唤醒所有等待线程
  • await():如果 state != 0,当前线程阻塞等待

适用场景:主线程等待多个子任务完成后继续执行。

5.3 共享模式获取流程(acquireShared)
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  // ① 子类实现:尝试获取共享锁
        doAcquireShared(arg);        // ② 获取失败→入队
}

共享模式与独占模式的区别:

  • 独占模式:tryAcquire 返回 boolean(成功/失败)
  • 共享模式:tryAcquireShared 返回 int(正数=还有剩余资源,0=刚好获取完,负数=失败)

六、AQS 在 JUC 中的全景图

在这里插入图片描述


七、生产级避坑清单

✅ AQS 与同步器使用避坑指南
1. 不要直接使用 AQS → 用它的子类(ReentrantLock、Semaphore、CountDownLatch)
2. CountDownLatch 计数器不能重置 → 需要重置用 CyclicBarrier
3. Semaphore 获取许可后记得释放 → try-finally 保证 release()
4. 公平锁 vs 非公平锁:公平锁保证 FIFO,吞吐量更低;非公平锁允许插队,吞吐量更高
5. ReentrantLock 必须在 finally 中 unlock() → 否则锁永不释放
6. tryLock() 带超时 → 获取失败可以做降级或重试,避免线程永久阻塞
7. Condition.await() 必须在 lock() 后调用 → 否则抛 IllegalMonitorStateException

八、面试高频考点

Q1:AQS 是什么?核心思想是什么?

AQS 是 AbstractQueuedSynchronizer 的缩写,JUC 并发包的底层基石。核心思想是:用一个 volatile int state 表示同步状态,用一个 FIFO 双向队列(CLH 队列)管理等待线程,通过模板方法模式让子类只需实现 tryAcquire/tryRelease 等少量方法,就能构建自定义同步器。

Q2:AQS 支持哪两种模式?分别对应哪些实现?

独占模式(Exclusive):同一时刻只有一个线程能获取同步状态,典型实现是 ReentrantLock。共享模式(Shared):同一时刻多个线程可以同时获取同步状态,典型实现是 Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁。

Q3:ReentrantLock 的可重入性是如何实现的?

tryAcquire 方法中,先判断 state 是否为 0(锁空闲),如果是则 CAS 抢锁;如果 state > 0,检查当前线程是否已经是锁的持有者,如果是则直接 state++(可重入),如果不是则获取失败。解锁时每次 state--,直到 state 归零才真正释放锁。

Q4:AQS 的 CLH 队列是什么?

CLH 队列是一个 FIFO 双向链表,用于存储等待获取同步状态的线程。每个线程被封装成 Node 节点,包含线程引用、等待状态(waitStatus)、前驱和后继指针。获取同步状态失败的线程会入队等待,释放锁时唤醒队首线程。

Q5:CountDownLatch 和 Semaphore 的区别?

CountDownLatch:state 表示倒计数,countDown() 减 1,state 归零时唤醒所有等待线程,计数器不能重置。Semaphore:state 表示剩余许可数,acquire() 减 1,release() 加 1,许可数可以动态增减,用于限流控制并发访问数。

Q6:AQS 如何避免 ABA 问题?

AQS 使用 CAS 操作更新 state,CAS 本身存在 ABA 问题,但 AQS 的 state 是整型计数器(单调增/减),ABA 问题不会影响锁的正确性——state 的值变化代表着“锁被释放了又被获取”,对于同步语义来说,只要最终状态正确即可。需要 ABA 敏感的原子操作应使用 AtomicStampedReference


面试官追问陷阱(加分题)

追问1:“tryAcquireacquire 的区别是什么?”

👉 tryAcquire 是子类需要实现的具体逻辑(尝试获取同步状态,返回 boolean),由子类根据 state 判断能否获取。acquire 是 AQS 提供的模板方法,封装了“尝试获取→失败入队→阻塞等待”的完整流程,子类不能重写。

追问2:“AQS 的共享模式和独占模式在队列处理上有什么区别?”

👉 独占模式释放锁时只唤醒一个后继线程(unparkSuccessor)。共享模式释放锁时,会传播唤醒——如果当前节点的 waitStatusPROPAGATE,会继续唤醒后继节点,让多个等待线程同时获取共享锁。

追问3:“ReentrantLock 的公平锁和非公平锁在 AQS 层面有什么区别?”

👉 非公平锁的 tryAcquire 中:CAS 抢锁前不检查 CLH 队列中是否有等待线程,直接尝试 CAS——这就是“插队”。公平锁的 tryAcquire 中:如果 CLH 队列中有等待线程,当前线程直接失败入队,不抢锁,保证 FIFO 顺序。其他流程(入队、阻塞、唤醒)完全一致。


九、练习题

  1. 分析题:AQS 的 CLH 队列为什么需要设计成双向链表而不是单向链表?

    💡 思路:需要支持取消等待(CANCELLED 节点出队),单向链表无法从后往前删除;同时 Condition 等待队列的节点转移时需要前驱指针。

  2. 源码题:阅读 ReentrantLock 的非公平锁 tryAcquire 源码,画出加锁失败时线程入队和阻塞的完整流程。

  3. 场景设计:某系统需要对数据库连接池做限流,最多同时允许 10 个线程获取连接。用 Semaphore 如何实现?

    💡 思路:Semaphore semaphore = new Semaphore(10); 获取连接前 semaphore.acquire(),归还连接时 semaphore.release(),用 try-finally 保证释放。

📊 你的学习进度

  • 当前:第54篇 / 共108篇 · 进阶篇:并发编程与JUC详解(第51~60篇)
  • ✅ 已完成:基础篇44篇 + 第45~54篇
  • 📖 正在学:第54篇
  • ⏳ 待学习:第55~108篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

👉 下一篇文章预告

🚀 下一篇:《第55篇:线程池ThreadPoolExecutor

内容简介:七大参数深度解析、四种拒绝策略、任务队列选型、动态调优、Executors 工厂方法的陷阱(无界队列 OOM)、实际生产案例。

👉 多线程专题持续深入,拿下线程池核心!

📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值