AQS 原理主线:state、CLH 队列、独占/共享与实战排查

AQS(AbstractQueuedSynchronizer)几乎是 Java 并发面试的"主干题"。

你只要把 AQS 讲清楚,很多同步工具(ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock)都能顺着讲出来。

这篇按一条主线讲透:

  • AQS 解决什么问题
  • state 是什么
  • 线程怎么排队(CLH 变体)
  • 独占/共享的差异
  • 常见追问 + 线上排查

你可以用一句话把 AQS 的价值说清:

  • 把"抢不到就排队、排队就阻塞、释放就唤醒"的通用流程做成框架,只把 state 的语义留给子类。

1. AQS 解决什么问题

AQS 的目标不是"提供锁",而是提供一个可复用的同步器框架

  • 用一个整数状态 state 表示同步状态
  • 用一个 FIFO 队列管理获取失败的线程
  • 通过模板方法把"排队/阻塞/唤醒"这套通用流程复用出来

你可以把 AQS 理解成:

  • 状态机(state) + 等待队列(queue) + 阻塞/唤醒(park/unpark)

工程上可以再落到一个更直接的心智模型:

  • 快路径:CAS 抢 state 成功(无竞争)
  • 慢路径:CAS 失败 -> 入队 -> park -> 被唤醒后重试

2. state:同步器的核心状态

AQS 内部维护一个 volatile int state

它的含义由具体同步器定义:

  • ReentrantLockstate 表示重入次数(0=未锁,>0=持有锁)
  • CountDownLatchstate 表示计数器(>0 需要等待,0 放行)
  • Semaphorestate 表示剩余许可数

关键点:

  • state 的修改必须是原子的(CAS)
  • state 的读写与队列配合,决定线程是"直接成功"还是"进入队列等待"

你可以把 state 看成"门票":

  • 独占:门票只能被一个人持有
  • 共享:门票可以被多人消耗/归还(许可)或倒计时归零后全员放行

3. AQS 的队列:CLH 变体(你只需掌握行为)

AQS 维护一个 FIFO 的等待队列(Node 链表)。

当线程获取同步状态失败时:

  • 会被封装成一个 Node 入队
  • 进入等待
  • 前驱节点释放后,唤醒后继节点

你不用背 Node 的字段,但要会讲清这三点:

  • 为什么要排队:避免大量线程自旋浪费 CPU
  • 为什么 FIFO:保证公平性基础(是否严格公平由实现决定)
  • 为什么只唤醒后继:减少"惊群"

再补一个工程理解点:

  • 很多线程栈里看到 LockSupport.park 并不等于"死锁",可能只是 AQS 的正常排队阻塞。

4. 独占模式(Exclusive):以 ReentrantLock 为例

独占模式的核心语义:

  • 同一时刻只能有一个线程持有同步状态

典型流程:

  1. 线程尝试 CAS 抢 state(从 0 改成 1)
  2. 成功 -> 获得锁
  3. 失败 -> 入队 -> park
  4. 前驱释放 -> unpark -> 再次尝试获取

与可重入的关系:

  • 如果当前线程已经持有锁,再次获取时直接 state++

5. 用最小代码把 AQS"跑起来":自定义独占锁(Mutex)

如果你想真正把 AQS 吃透,最有效的方法就是写一个最小可用的独占锁。

下面示例是一个不可重入的 Mutex(可运行骨架):

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class Mutex implements Lock {
  private static class Sync extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int acquires) {
      // state: 0 未占用,1 已占用
      if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
      }
      return false;
    }

    @Override
    protected boolean tryRelease(int releases) {
      if (getState() == 0) throw new IllegalMonitorStateException();
      setExclusiveOwnerThread(null);
      setState(0);
      return true;
    }

    @Override
    protected boolean isHeldExclusively() {
      return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
    }

    Condition newCondition() {
      return new ConditionObject();
    }
  }

  private final Sync sync = new Sync();

  @Override
  public void lock() {
    sync.acquire(1);
  }

  @Override
  public boolean tryLock() {
    return sync.tryAcquire(1);
  }

  @Override
  public void unlock() {
    sync.release(1);
  }

  @Override
  public Condition newCondition() {
    return sync.newCondition();
  }

  @Override
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }

  @Override
  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(time));
  }
}

你从这个例子里要抓住两点:

  • acquire/release 是 AQS 帮你做的"排队/park/unpark"框架
  • 你只需要定义 tryAcquire/tryRelease 的 state 语义

6. 共享模式(Shared):以 CountDownLatch/Semaphore 为例

共享模式的核心语义:

  • 同一时刻允许多个线程同时通过(取决于 state/许可数)

典型例子:

  • CountDownLatch:state=0 时所有 await 线程放行
  • Semaphore:state 代表许可数,拿到许可才能通过

共享模式的一个重要点:

  • 释放时可能需要唤醒多个等待线程(例如 latch 归零)

共享模式你可以用 latch 的语义来记:

  • state > 0:所有 await 都要排队
  • state == 0:所有 await 都直接通过

7. AQS 的"模板方法"你要会怎么说

面试讲法(不背源码也能讲清):

  • AQS 负责:排队、阻塞、唤醒、超时/中断处理
  • 子类负责:
    • tryAcquire/tryRelease(独占)
    • tryAcquireShared/tryReleaseShared(共享)

所以你可以把它说成:

  • AQS 把通用的等待队列与线程调度做成框架,把"能不能拿到同步状态"的判断留给子类。

8. 常见追问(高频)

8.1 为什么用 CAS + 队列,而不是纯 synchronized

  • CAS 快路径:无竞争时极低成本
  • 队列阻塞:竞争激烈时避免 CPU 自旋浪费

8.2 AQS 怎么处理中断/超时

  • park 等待期间线程可能被中断
  • AQS 会在合适时机检查中断标记,并按 API 语义决定:
    • InterruptedException
    • 或返回失败

8.3 公平锁与非公平锁差在哪

  • 公平:倾向让队列头先获取
  • 非公平:允许"插队"抢占,吞吐更高但延迟抖动可能更大

你可以再补一句面试加分点:

  • 非公平锁会先 CAS 抢一次 state(插队),失败了再走队列。

9. 线上排查:怎么判断是不是 AQS 锁竞争

你在线上遇到 RT 抖动/吞吐下降时,常见信号:

  • 大量线程 BLOCKED / WAITING (parking)
  • jstack 中出现 java.util.concurrent.locks.AbstractQueuedSynchronizer 相关栈

排查路径建议:

  1. 先抓线程栈:看大量线程是否卡在同一个锁竞争点
  2. 确认锁对象/锁粒度:是否把 IO、RPC 包在锁里
  3. 看是否有锁顺序问题:是否存在死锁风险

更细的关键词(jstack 常见信号):

  • java.util.concurrent.locks.LockSupport.park
  • AbstractQueuedSynchronizer$ConditionObject.await
  • AbstractQueuedSynchronizer.acquire / acquireQueued

10. 面试表达(30 秒讲清楚)

  • AQS 是同步器框架,用 state 表示同步状态,用 FIFO 队列管理获取失败的线程。
  • 无竞争走 CAS 快路径,竞争时入队 park,释放时 unpark 后继。
  • 独占模式对应 ReentrantLock(同一时刻一个线程),共享模式对应 CountDownLatch/Semaphore(允许多个线程通过)。
  • AQS 负责排队与线程调度,子类通过 tryAcquire/tryRelease 定义 state 的语义。
  • 排查锁竞争:看线程 parking、jstack 是否落在 AQS 相关栈,再回到锁粒度与临界区。

11. 总结

  • AQS = state + 队列 + park/unpark
  • 独占/共享是两种不同的获取/释放语义
  • 讲清 AQS,ReentrantLock/CountDownLatch/Semaphore 都能顺着讲
  • 工程排查核心:找到竞争点、缩小临界区、避免在锁内做慢操作
相关推荐
2301_816651222 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python
2401_873544922 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
小江的记录本2 小时前
【Redis】Redis常用命令速查表(完整版)
java·前端·数据库·redis·后端·spring·缓存
add45a2 小时前
C++中的组合模式
开发语言·c++·算法
卓怡学长2 小时前
m281基于SSM框架的电脑测评系统
java·数据库·spring·tomcat·maven·intellij-idea
dys_Codemonkey2 小时前
ROS 2 环境配置与 Shell 配置文件详解(zsh/bash)ROS 2 多工作空间规范配置
开发语言·chrome·bash
umeelove352 小时前
SQL中的DISTINCT、SQL DISTINCT详解、DISTINCT的用法、DISTINCT注意事项
java·数据库·sql
2501_945423542 小时前
模板编程中的SFINAE技巧
开发语言·c++·算法
AMoon丶2 小时前
Golang--垃圾回收
java·linux·开发语言·jvm·后端·算法·golang