Skip to content

AQS 与 JUC 工具类全过程原理

AQS 是 Java 并发里非常关键、也非常容易学成“背八股”的知识点。很多人会背:

AQS 有一个 state 和一个 FIFO 队列,ReentrantLock、CountDownLatch、Semaphore 都基于 AQS。

但真正面试或排查时,这句话远远不够。你要能说清:

  1. state 到底代表什么。
  2. 线程抢不到资源为什么要入队。
  3. 入队后为什么要 park
  4. 释放资源时为什么要 unpark 后继线程。
  5. 独占模式和共享模式有什么区别。
  6. ReentrantLock、Semaphore、CountDownLatch 为什么行为不同但底层都能用 AQS。
  7. 公平锁为什么吞吐低一些,非公平锁为什么可能插队。
  8. 线上线程 WAITINGBLOCKED、锁等待时怎么联系到 AQS/JUC。

这一页把这些过程串起来。

学习目标

学完这一页,你要能回答:

  1. AQS 是什么,为什么 JUC 要抽象出 AQS。
  2. state、同步队列、NodewaitStatuspark/unpark 分别负责什么。
  3. 独占获取和释放资源的完整流程。
  4. 共享获取和释放资源的完整流程。
  5. ReentrantLock 如何通过 state 实现可重入。
  6. CountDownLatch 如何通过 state 实现等待多个任务完成。
  7. Semaphore 如何通过 state 实现许可证限流。
  8. Condition 条件队列和 AQS 同步队列有什么区别。
  9. 公平锁和非公平锁的底层差异。
  10. 商业项目里如何选择 ReentrantLock、CountDownLatch、Semaphore,而不是乱用。

为什么需要 AQS

并发工具经常要解决同一类问题:

多个线程竞争有限资源,拿到资源的继续执行,拿不到资源的排队等待,资源释放后再唤醒等待线程。

如果每个工具都自己写一套排队、阻塞、唤醒、中断、超时、取消逻辑,会非常复杂,也容易出错。

mermaid
flowchart TD
    A["并发工具共同问题"] --> B["资源状态怎么表示"]
    A --> C["抢不到资源怎么排队"]
    A --> D["线程如何安全阻塞"]
    A --> E["资源释放后唤醒谁"]
    A --> F["中断/超时/取消怎么处理"]
    B --> G["AQS 抽象通用骨架"]
    C --> G
    D --> G
    E --> G
    F --> G

AQS 的定位是:

AQS 不直接规定“什么是锁”,它提供一套同步器骨架。具体子类只需要定义“怎样算获取成功”和“怎样算释放成功”。

AQS 的三个核心部件

mermaid
flowchart TD
    A["AQS"] --> B["volatile int state<br/>同步状态"]
    A --> C["CLH/FIFO 双向队列<br/>保存等待线程"]
    A --> D["LockSupport<br/>park/unpark 阻塞和唤醒"]
部件可以理解成作用
state资源数量或锁状态判断当前资源能不能被获取
同步队列排队区抢不到资源的线程在这里排队
LockSupport停车/叫醒机制让线程不占 CPU 地等待,释放后唤醒

不同工具对 state 的解释不同:

工具state 代表什么什么时候线程通过
ReentrantLock锁被重入的次数state == 0 或当前线程就是持有者
Semaphore剩余许可证数量state > 0 且 CAS 扣减成功
CountDownLatch剩余倒计数state == 0
ReentrantReadWriteLock高位读锁数量,低位写锁重入次数按读写锁兼容规则判断

为什么 state 要 volatile + CAS

state 会被多个线程同时读写。只用普通变量会有两个问题:

  1. 一个线程改了,其他线程可能看不到。
  2. 多个线程同时改,可能互相覆盖。

AQS 通过 volatile 保证可见性,通过 CAS 保证原子修改:

java
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    // 底层是 CAS,只有当前值等于 expect 时才更新成 update
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

举例:Semaphore 还有 1 个许可证,两个线程同时来拿。

mermaid
flowchart TD
    A["state = 1"] --> B["线程 A 看到还有许可"]
    A --> C["线程 B 看到还有许可"]
    B --> D["CAS 1 -> 0 成功"]
    C --> E["CAS 1 -> 0 失败"]
    D --> F["线程 A 获取许可"]
    E --> G["线程 B 进入等待或重试"]

如果没有 CAS,两个线程都可能以为自己拿到了最后一个许可证,限流就失效了。

同步队列是什么

线程抢不到资源,不能一直自旋消耗 CPU。AQS 会把它包装成 Node 放进同步队列。

mermaid
flowchart TD
    A["head<br/>哨兵节点"] --> B["Node A<br/>等待线程 A"]
    B --> C["Node B<br/>等待线程 B"]
    C --> D["tail<br/>队尾"]

队列节点里通常关心:

字段作用
prev指向前驱节点
next指向后继节点
thread当前节点对应的线程
waitStatus节点等待状态
nextWaiter条件队列或共享模式标记

为什么要有队列?

如果没有队列后果
抢不到锁的线程全部忙等CPU 被自旋浪费
不记录等待顺序唤醒谁不确定,容易混乱
不能处理中断和取消超时等待、取消等待很难实现
不能支持公平策略无法判断前面有没有人排队

waitStatus 怎么理解

waitStatus 是节点状态,常见值:

状态含义简单理解
0初始状态刚入队,还没明确后续动作
SIGNAL = -1后继节点需要被唤醒当前节点释放时要通知后面
CANCELLED = 1节点取消等待超时、中断或异常退出,不再竞争
CONDITION = -2在条件队列中等待 Condition.signal
PROPAGATE = -3共享模式继续传播共享释放时继续唤醒后继

不用一开始背源码细节。先记住:

负数通常表示节点还有效并需要协作唤醒;正数 CANCELLED 表示节点已经取消。

独占模式:ReentrantLock 的加锁流程

独占模式表示同一时刻只允许一个线程持有资源。

典型工具:ReentrantLock

mermaid
flowchart TD
    A["线程调用 lock"] --> B["tryAcquire"]
    B --> C{"获取成功"}
    C -- "是" --> D["执行业务代码"]
    C -- "否" --> E["封装成 Node 入队"]
    E --> F{"前驱是否 head"}
    F -- "是" --> G["再次 tryAcquire"]
    G --> H{"是否成功"}
    H -- "是" --> I["当前节点成为 head"]
    H -- "否" --> J["park 挂起"]
    F -- "否" --> J
    J --> K["等待前驱释放后 unpark"]
    K --> F

关键点:

  1. 线程不会失败一次就马上睡眠,会先入队。
  2. 入队后只有前驱是 head 的节点才有资格再次尝试获取。
  3. 获取失败后才 park,避免无意义消耗 CPU。
  4. 被唤醒后不是直接拥有锁,而是重新竞争。

为什么被唤醒后还要重新竞争?

因为唤醒只表示“你可以再试试”,不等于“锁一定归你”。在非公平锁中,新来的线程可能插队抢到锁;同时也可能存在中断、取消等情况。

独占模式:ReentrantLock 的释放流程

mermaid
flowchart TD
    A["线程调用 unlock"] --> B["tryRelease"]
    B --> C["state 减 1"]
    C --> D{"state 是否为 0"}
    D -- "否" --> E["只是减少重入次数<br/>仍然持有锁"]
    D -- "是" --> F["清空 owner"]
    F --> G["找到有效后继节点"]
    G --> H["LockSupport.unpark 唤醒后继线程"]
    H --> I["后继线程重新竞争锁"]

可重入锁最重要的点:

同一个线程加锁几次,就必须解锁几次。只有 state 减到 0,锁才真正释放。

错误示例:

java
lock.lock();
lock.lock();
try {
    // do something
} finally {
    lock.unlock();
}

这段代码只释放了一次,state 还没归零,其他线程会一直等。

正确写法:

java
lock.lock();
try {
    lock.lock();
    try {
        // do something
    } finally {
        lock.unlock();
    }
} finally {
    lock.unlock();
}

真实业务中不建议故意写复杂重入层级。更常见的是一个加锁方法内部调用另一个也需要同一把锁的方法,可重入避免自己把自己锁死。

公平锁和非公平锁

非公平锁

非公平锁允许新来的线程先尝试抢锁。

mermaid
flowchart TD
    A["锁刚释放"] --> B["队列中有等待线程"]
    A --> C["新线程刚好来抢锁"]
    C --> D{"CAS 抢锁是否成功"}
    D -- "成功" --> E["新线程插队成功"]
    D -- "失败" --> F["进入队列"]

优点:

  1. 减少线程频繁挂起和唤醒。
  2. 吞吐量通常更高。

缺点:

  1. 等待线程可能被插队。
  2. 极端情况下等待时间不稳定。

公平锁

公平锁获取锁前会检查队列前面是否有人。

mermaid
flowchart TD
    A["线程请求锁"] --> B{"同步队列前面是否有等待线程"}
    B -- "有" --> C["排队等待"]
    B -- "没有" --> D["尝试 CAS 获取锁"]

公平锁更接近先来先服务,但吞吐可能降低。原因是它更依赖排队唤醒,减少了“刚好正在运行的线程顺手拿锁”的机会。

商业项目默认一般用非公平锁。只有在确实需要等待顺序可控、避免长期饥饿时,才考虑公平锁。

共享模式:Semaphore 的流程

共享模式表示资源可以被多个线程同时获取,只要资源数量足够。

典型工具:Semaphore

假设 Semaphore semaphore = new Semaphore(3)state = 3 表示还有 3 个许可证。

mermaid
flowchart TD
    A["线程 acquire"] --> B["读取 state"]
    B --> C{"state 是否大于 0"}
    C -- "是" --> D["CAS state - 1"]
    D --> E{"CAS 是否成功"}
    E -- "成功" --> F["获取许可,继续执行"]
    E -- "失败" --> B
    C -- "否" --> G["进入 AQS 队列等待"]
    H["线程 release"] --> I["CAS state + 1"]
    I --> J["唤醒等待线程"]

Semaphore 适合做并发数限制:

java
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class HospitalApiLimitDemo {
    private static final Semaphore LIMITER = new Semaphore(5);

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            int taskId = i;
            new Thread(() -> callHospital(taskId)).start();
        }
    }

    private static void callHospital(int taskId) {
        boolean acquired = false;
        try {
            acquired = LIMITER.tryAcquire(2, TimeUnit.SECONDS);
            if (!acquired) {
                System.out.println("task " + taskId + " rejected by local limiter");
                return;
            }
            System.out.println("task " + taskId + " calling hospital api");
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (acquired) {
                LIMITER.release();
            }
        }
    }
}

注意:

  1. acquire 成功后必须 release
  2. 推荐关键业务使用 tryAcquire(timeout),不要无限等。
  3. Semaphore 控制的是本 JVM 内并发,不是分布式限流。

共享模式:CountDownLatch 的流程

CountDownLatch 用来等待多个任务完成。

假设 new CountDownLatch(3),初始 state = 3

mermaid
flowchart TD
    A["主线程 await"] --> B{"state 是否为 0"}
    B -- "是" --> C["直接通过"]
    B -- "否" --> D["进入 AQS 共享等待队列"]
    E["任务 1 countDown"] --> F["state - 1"]
    G["任务 2 countDown"] --> F
    H["任务 3 countDown"] --> I["state 变为 0"]
    I --> J["唤醒 await 等待线程"]
    J --> C

为什么 CountDownLatch 不能复用?

因为它的 state 只会从初始值不断减到 0,没有重置机制。归零后所有等待线程都会通过,后续再 await 也会直接通过。

Demo:

java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CollectSummaryDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 1; i <= 3; i++) {
            int taskId = i;
            pool.execute(() -> {
                try {
                    System.out.println("collect part " + taskId);
                } finally {
                    latch.countDown();
                }
            });
        }

        boolean finished = latch.await(3, TimeUnit.SECONDS);
        if (!finished) {
            System.out.println("collect timeout, need compensation");
        } else {
            System.out.println("all collect tasks finished");
        }
        pool.shutdown();
    }
}

关键点:

  1. countDown() 要放在 finally,否则任务异常会导致主线程一直等。
  2. 关键链路建议使用带超时的 await
  3. 需要复用屏障时,用 CyclicBarrierPhaser

Condition:条件队列和同步队列

Condition 常和 ReentrantLock 搭配使用。它解决的是:

线程已经拿到锁,但某个业务条件不满足,需要释放锁并等待条件变化。

典型例子:队列为空时消费者等待;队列不为空时生产者唤醒消费者。

mermaid
flowchart TD
    A["线程持有 ReentrantLock"] --> B{"条件是否满足"}
    B -- "满足" --> C["继续执行"]
    B -- "不满足" --> D["condition.await"]
    D --> E["释放锁"]
    E --> F["进入 Condition 条件队列"]
    G["其他线程 signal"] --> H["从条件队列转移到 AQS 同步队列"]
    H --> I["重新竞争锁"]
    I --> C

注意:signal 不是让线程直接继续执行业务,而是把它从条件队列转移到同步队列。它还要重新竞争锁。

正确写法要用 while 检查条件:

java
lock.lock();
try {
    while (queue.isEmpty()) {
        notEmpty.await();
    }
    return queue.remove(0);
} finally {
    lock.unlock();
}

为什么不用 if

因为线程被唤醒后,条件可能又被其他线程改变;也可能存在虚假唤醒。用 while 可以重新检查条件。

ReentrantLock、synchronized 怎么选

对比synchronizedReentrantLock
使用复杂度简单,JVM 自动释放必须手动 unlock
可中断获取不支持支持 lockInterruptibly
超时获取不支持支持 tryLock(time)
公平锁不支持直接配置支持公平/非公平
多条件队列一个对象 Monitor 条件队列可以创建多个 Condition
日常业务优先使用需要高级能力时使用

选择建议:

  1. 普通互斥代码块,用 synchronized 更简单。
  2. 需要超时、可中断、公平锁、多条件队列,用 ReentrantLock
  3. 不要为了“高级”滥用 ReentrantLock,忘记 unlock 的后果很严重。

线上排查:AQS/JUC 相关问题怎么看

大量线程 WAITING

如果线程栈显示:

text
java.lang.Thread.State: WAITING
at jdk.internal.misc.Unsafe.park
at java.util.concurrent.locks.LockSupport.park

可能是:

  1. 等待 AQS 锁。
  2. 等待 CountDownLatch.await
  3. 等待 Semaphore.acquire
  4. 线程池 Worker 在队列中等待任务。

不要只看 WAITING,要继续看栈上方是哪一个类。

栈上方类可能问题
ReentrantLock.lock锁竞争
CountDownLatch.await某些任务没 countDown
Semaphore.acquire许可证耗尽或忘记 release
LinkedBlockingQueue.take线程池空闲等待任务

大量线程 BLOCKED

BLOCKED 通常和 synchronized Monitor 竞争有关,不是 AQS 的 park 等待。

处理方向:

  1. 找到持有锁的线程。
  2. 看锁内是否有慢 SQL、远程调用、文件 IO。
  3. 缩小锁范围或改用并发容器。

CountDownLatch 一直不返回

常见原因:

  1. 子任务异常后没有执行 countDown
  2. 任务根本没有提交成功。
  3. 线程池满了,子任务还在队列中。
  4. await 没有设置超时。

排查流程:

mermaid
flowchart TD
    A["await 一直等待"] --> B["检查 countDown 是否 finally"]
    B --> C["检查子任务是否执行"]
    C --> D["检查线程池队列和拒绝次数"]
    D --> E["检查是否有子任务异常"]
    E --> F["给 await 加超时和补偿"]

Semaphore 许可泄漏

许可泄漏通常是 acquire 成功后业务异常,没有 release

错误写法:

java
semaphore.acquire();
doWork();
semaphore.release();

正确写法:

java
boolean acquired = false;
try {
    semaphore.acquire();
    acquired = true;
    doWork();
} finally {
    if (acquired) {
        semaphore.release();
    }
}

商业场景怎么用

场景一:接口聚合等待多个结果

订单详情页需要同时查用户、订单、优惠券,可以用线程池 + CountDownLatch 等待结果汇总。但要设置超时,不能一个下游慢导致整个接口永久等待。

场景二:限制医院接口采集并发

外部医院接口承受能力有限,可以用 Semaphore 在单个服务实例内限制同时调用数量。分布式多实例场景还要配合 Redis 限流、网关限流或 Sentinel。

场景三:复杂队列通知

本地内存队列中,消费者等待“不为空”,生产者等待“不满”,可以用 ReentrantLock + 两个 Condition,比单个 wait/notify 更清晰。

场景四:热点资源保护

某个设备采集协议同一时间只能一个线程操作,可以用 ReentrantLock 或按设备维度的锁映射。但要避免锁对象无限增长,注意过期清理。

面试标准回答

AQS 是什么

标准回答:

text
AQS 是 AbstractQueuedSynchronizer,是 JUC 并发工具的同步器基础框架。它用 volatile int state 表示同步状态,用 FIFO 双向队列保存等待线程,用 CAS 修改状态,用 LockSupport.park/unpark 阻塞和唤醒线程。具体工具只需要实现 tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared 等方法定义资源获取和释放规则。ReentrantLock 把 state 当作锁重入次数,Semaphore 把 state 当作许可证数量,CountDownLatch 把 state 当作剩余倒计数。

追问点:

  1. 为什么 AQS 需要队列?
  2. state 为什么要 volatile + CAS?
  3. 独占模式和共享模式区别是什么?
  4. unpark 的线程是不是马上拿到锁?

ReentrantLock 为什么可重入

标准回答:

text
ReentrantLock 基于 AQS 实现,可重入依赖 state 和独占持有线程。第一次加锁时,如果 state 为 0,线程 CAS 把 state 改成 1 并设置 owner。后续同一个线程再次加锁时,不需要排队,而是把 state 加 1。释放锁时 state 减 1,只有减到 0 才清空 owner 并唤醒后继线程。所以同一个线程加锁几次,就必须 unlock 几次。

CountDownLatch 和 Semaphore 底层区别

标准回答:

text
它们都基于 AQS 共享模式,但 state 含义不同。CountDownLatch 的 state 表示剩余倒计数,await 时如果 state 不为 0 就进入等待队列,countDown 把 state 减到 0 后唤醒等待线程,且不能复用。Semaphore 的 state 表示剩余许可证,acquire 时 state 大于 0 才能扣减成功,否则等待;release 会归还许可证并唤醒等待线程。CountDownLatch 解决等待完成,Semaphore 解决并发限流。

关联知识点

知识点说明
AQS原有 AQS 源码笔记
ReentrantLock可重入锁、公平锁、非公平锁
ReentrantReadWriteLock读写锁和共享/独占思想
CAS 原理CAS 如何保证原子更新
JUC 工具类CountDownLatch、Semaphore、CyclicBarrier
线程池生命周期与排查线程状态、队列堆积和 jstack 排查
JavaSE 面试标准回答和追问

本章小结

AQS 的核心不是背源码字段,而是理解同步器的通用模型:用 state 表示资源状态,用 CAS 修改状态,用队列保存等待线程,用 park/unpark 控制阻塞和唤醒。ReentrantLock、Semaphore、CountDownLatch 的差异,本质上是它们对 state 的解释不同,以及使用独占模式还是共享模式不同。掌握这条主线后,JUC 就不再是一堆零散 API。