AQS

1. AQS 到底是什么

AQS 全称是 AbstractQueuedSynchronizer

它是一个 同步器框架,用来帮助开发者快速实现各种锁和同步组件。

你可以把它理解成:

AQS 提供了一套通用的“排队 + 竞争状态 + 阻塞唤醒”机制,具体这个同步器是锁、信号量、倒计时器,交给子类自己定义。

所以 AQS 干的事情主要有三件:

  • 用一个 state 表示同步状态

  • 用一个双向队列管理抢不到锁的线程

  • 提供 acquire / release 这类模板方法,子类只要实现 tryXxx 即可


2. 为什么 AQS 这么重要

因为 Java 里很多并发工具底层都靠它:

  • ReentrantLock

  • Semaphore

  • CountDownLatch

  • ReentrantReadWriteLock

  • ConditionObject

也就是说,AQS 有点像一个 并发世界的脚手架
你不需要每次都手搓:

  • 谁抢锁成功

  • 谁入队等待

  • 谁该阻塞

  • 谁该被唤醒

  • 队列怎么维护

这些脏活累活 AQS 给你包了。


3. AQS 的设计思想

AQS 的思想很优雅,核心是 模板方法模式

它把同步器拆成两层:

第一层:通用框架层

AQS 自己负责:

  • state 状态管理

  • 等待队列管理

  • 线程入队

  • 线程挂起 park

  • 线程唤醒 unpark

  • 独占 / 共享模式的流程控制

第二层:子类自定义规则

子类只需要告诉 AQS:

  • 当前这个同步器,怎么判断能不能拿到资源

  • 当前这个同步器,怎么释放资源

比如这些方法:

1
2
3
4
5
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
protected boolean isHeldExclusively()

也就是说:

AQS 说,排队和调度我来做,资源能不能拿你来说。

这个分工特别妙。


4. AQS 最核心的两个东西

4.1 state,同步状态

AQS 里有一个非常关键的变量:

private volatile int state;

它表示同步状态。

这个状态的含义由子类决定。

不同组件里 state 的含义不同

ReentrantLock

  • 0 表示没锁

  • 1 表示已被某线程持有

  • 大于 1 表示重入次数

Semaphore

  • state 表示剩余许可证数量

CountDownLatch

  • state 表示剩余倒计时次数

ReentrantReadWriteLock

  • state 高低位拆分,分别表示读锁和写锁状态

所以你要记住一句:

AQS 只提供 state 这个容器,具体语义由子类赋予。


4.2 CLH 变体双向等待队列

AQS 内部维护了一个 FIFO 双向链表队列,一般说它是基于 CLH queue 思想实现的变体。

它的作用是:

  • 抢资源失败的线程进队

  • 按顺序排队等待

  • 前驱节点释放后,唤醒后继节点

队列节点大概长这样:

1
2
3
4
5
6
7
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}

几个关键字段:

  • prev:前驱节点

  • next:后继节点

  • thread:当前节点绑定的线程

  • waitStatus:当前节点等待状态

  • nextWaiter:用于区分独占/共享模式,或者条件队列


5. AQS 队列里的 Node 是什么

每个等待线程会被包装成一个 Node 节点。

你可以理解成:

  • 线程抢锁失败

  • AQS 给它发一个号码牌 Node

  • 让它去队尾排队

  • 等前面的人处理完,再轮到它


6. waitStatus 是什么

这个字段面试里也挺常见。

常见值有这些:

  • 0:默认状态

  • SIGNAL = -1:当前节点的后继需要被唤醒

  • CANCELLED = 1:线程取消等待

  • CONDITION = -2:节点在条件队列中

  • PROPAGATE = -3:共享模式下传播唤醒

你不用死记所有值,先抓主线:

最重要的两个

  • SIGNAL:告诉前驱,释放时记得唤醒我后面的线程

  • CANCELLED:这个节点废了,别管它了


7. AQS 的两种模式

AQS 支持两种资源获取模式:

7.1 独占模式 Exclusive

同一时刻只能一个线程拿到资源。

比如:

  • ReentrantLock

7.2 共享模式 Shared

同一时刻可以多个线程一起拿资源。

比如:

  • Semaphore

  • CountDownLatch 的 await 唤醒传播

  • ReentrantReadWriteLock 的读锁

这个分类非常关键,因为 AQS 的很多方法都分两套:

  • acquire / release 对应独占

  • acquireShared / releaseShared 对应共享


8. AQS 获取锁的整体流程

先讲最经典的 独占获取流程
acquire(int arg) 为例。

8.1 第一阶段,先直接抢

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

1
2
        selfInterrupt();
}

看这段代码,AQS 的思路非常清晰:

第一步

先调用子类实现的 tryAcquire(arg)

  • 成功,直接拿到资源,结束

  • 失败,说明有竞争,进入队列流程

这就是典型的 先抢一次,失败再排队


8.2 第二阶段,失败就入队

addWaiter(Node.EXCLUSIVE)

这一步会把当前线程封装成一个独占节点,插入等待队列尾部。

入队底层用的是 CAS 保证线程安全。

队列一般会有一个哨兵头节点 head,真正等待的线程从 head.next 开始。


8.3 第三阶段,入队后自旋等待

acquireQueued(node, arg)

这个方法很重要,它会让线程在队列里循环判断:

逻辑大致是

  1. 看看自己前驱是不是头节点

  2. 如果前驱是头节点,说明自己有资格再试一次抢锁

  3. 调用 tryAcquire

  4. 成功则自己变成新 head

  5. 失败则决定是否阻塞

  6. 阻塞后等前驱释放时唤醒自己

这里的思想很妙:

只有排在队头的线程,才有资格再次抢资源。

这样可以减少无意义竞争。


9. 为什么要阻塞,不一直自旋吗

你知道吗,这就是 AQS 很聪明的地方 ❤️

CAS 那类乐观锁喜欢一直自旋。
AQS 的场景更复杂,如果竞争持续很久,一直空转会把 CPU 烧得很难看。

所以 AQS 采用的是:

  • 短暂自旋检查

  • 条件合适就 park 挂起

  • 前驱释放时再 unpark 唤醒

这属于 自旋 + 阻塞 的结合体。


10. park / unpark 是什么

AQS 挂起线程用的是 LockSupport:

  • LockSupport.park() 挂起当前线程

  • LockSupport.unpark(thread) 唤醒指定线程

你可以把它理解成并发包里的“精准停车和叫醒”。

wait/notify 更底层,也更灵活。


11. 独占模式完整流程图

你脑子里可以这样想:

线程进来

tryAcquire 尝试抢资源

成功 -> 直接返回

失败

封装成 Node 加入队尾

循环判断:
前驱是不是 head?
是 -> 再次 tryAcquire
成功 -> 自己设为 head,出队
失败 -> park 挂起
否 -> park 挂起

前驱释放资源时 unpark 后继

被唤醒后继续循环尝试


12. AQS 释放锁的流程

独占释放是 release(int arg):

1
2
3
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
    if (h != null && h.waitStatus != 0)  
1
2
3
4
5
            unparkSuccessor(h);
return true;
}
return false;
}

流程是:

第一步

调用子类实现的 tryRelease(arg)

子类决定:

  • 资源是否真的完全释放了

  • 如果是可重入锁,是减计数还是彻底释放

第二步

如果确实释放成功,就唤醒 head 的后继节点

也就是:

  • 当前持锁线程放手

  • 叫醒排在最前面的等待线程来继续竞争


13. 为什么释放时唤醒的是后继节点

因为等待队列本质上是 FIFO。

AQS 想尽量保证:

  • 排前面的先获得再次尝试的资格

注意是 先唤醒先尝试,并不代表 100% 绝对公平,除非具体实现就是公平锁。


14. 公平锁和非公平锁在 AQS 里怎么体现

这个很经典。

非公平锁

线程一上来就先直接抢,不太管队列里有没有人。

比如 ReentrantLock 默认就是非公平锁。

好处:

  • 吞吐量高

  • 少一次排队判断

坏处:

  • 可能插队

公平锁

线程来时先看队列前面有没有人在等。

有的话,自己老实排队。

AQS 里通常通过 hasQueuedPredecessors() 来判断前面是否已有排队线程。


15. ReentrantLock 是怎么基于 AQS 实现的

这是理解 AQS 最好的例子。

ReentrantLock 内部有一个 Sync,它继承 AQS。

非公平锁 tryAcquire 逻辑核心思路

情况 1:state == 0

说明锁空闲,尝试 CAS 抢占:

  • CAS 成功,设置 owner 为当前线程

  • 获取成功

情况 2:锁已被当前线程持有

说明发生重入:

  • state++

  • 成功

情况 3:锁被别人持有

  • 失败

  • 进入 AQS 队列等待

所以 ReentrantLock 的重入本质就是:

同一线程再次获取锁时,发现 owner 还是自己,于是只增加 state 计数。

释放时则反过来:

  • state–

  • 直到减到 0,才真正释放锁并唤醒后继


16. CountDownLatch 为什么也能用 AQS

这个点很多人第一次看会觉得神奇。

CountDownLatch 用的是 共享模式

state 的含义

  • 表示剩余计数

await()

调用 acquireSharedInterruptibly(1)

如果 state != 0,当前线程进队等待。

countDown()

调用 releaseShared(1)

把 state 减 1。
当减到 0 时,说明条件满足,唤醒所有等待线程。

所以它不是“抢锁”,它是在复用 AQS 的:

  • state 管理

  • 共享队列唤醒机制


17. Semaphore 为什么能用 AQS

Semaphore 也走共享模式。

state 的含义

  • 当前可用许可证数量

acquire()

尝试把 state 减掉一个许可数

  • 成功,拿到许可证

  • 失败,说明没资源,入队等待

release()

把 state 加回去,然后唤醒后继节点继续竞争

所以共享模式下,大家竞争的不是“唯一锁”,而是“若干份资源”。


18. 共享模式和独占模式的区别到底在哪

独占模式

  • 同一时刻一个线程成功

  • 成功后其他线程继续等

  • 比如互斥锁

共享模式

  • 一次可能多个线程都成功

  • 一个线程成功后,可能还要继续传播唤醒后继

  • 比如信号量、读锁

所以共享模式里有个很关键的概念:

传播 propagation

如果资源还有剩余,就可以继续唤醒后面节点。

这就是 PROPAGATE 状态出现的背景。


19. Condition 是什么,和 AQS 什么关系

这个点超重要。

Condition 是用来做 条件等待队列 的。
通常和 Lock 配合使用,类似 Object.wait/notify 的升级版。

比如:

1
2
3
4
5
lock.lock();
try {
while (!conditionOK) {
condition.await();
}
// do something  
1
2
3
} finally {
lock.unlock();
}

AQS 里的 ConditionObject 是其内部类。


20. Condition 的本质

AQS 其实维护了两类队列:

1. 同步队列

抢锁失败的线程排这里

2. 条件队列

调用 await() 后,线程先进条件队列

条件队列里的线程特点

  • 它们在等某个条件成立

  • 暂时不参与抢锁

当别的线程调用 signal() 时:

  • 条件队列头节点被转移到同步队列

  • 然后去重新参与锁竞争

这个转移过程是 Condition 的精髓。


21. await 和 signal 流程

await()

线程当前必须先持有锁,然后:

  1. 把当前线程包装成条件节点,加入条件队列

  2. 释放当前持有的锁

  3. 挂起自己

  4. 等待被 signal

  5. 被转移到同步队列

  6. 重新竞争锁

  7. 拿到锁后从 await 返回

signal()

当前线程必须持有锁,然后:

  1. 从条件队列取出一个节点

  2. 转移到同步队列

  3. 后续让它参与正常的抢锁流程

所以:

await 不是直接唤醒后立刻执行,而是先回到同步队列重新抢锁。

这个非常容易考。


22. AQS 为什么要用双向链表

因为它需要高效处理这些操作:

  • 从尾部入队

  • 找前驱节点

  • 取消节点时跳过失效节点

  • 唤醒后继节点

单链表做这些会麻烦很多,尤其取消和回溯处理。


23. AQS 为什么 head 节点通常不存真实线程

因为 head 更多像一个 哨兵节点

当一个线程成功获取资源后,它会把自己所在节点设为新的 head。
原来的头节点会被丢弃。

head 的主要作用是:

  • 标识队列起点

  • 简化队列逻辑

  • head.next 成为实际等待的第一个线程


24. AQS 和 synchronized 的区别

这个面试也常问。

synchronized

  • JVM 层面的关键字

  • 底层依赖对象监视器 monitor

  • 使用更简单

  • 自动释放锁

  • 支持可重入

  • 条件等待靠 wait/notify

AQS

  • JDK 层面的同步器框架

  • 基于 CAS + volatile + CLH 队列 + park/unpark

  • 扩展性更强

  • 可以实现公平锁、非公平锁、共享锁、条件队列等复杂语义

你可以这么理解:

synchronized 更像现成家具,AQS 更像高质量工具箱。


25. AQS 底层靠哪些技术拼起来

这个可以当总结图记住:

volatile

保证 state、head、tail 等变量的可见性

CAS

保证入队、出队、修改 state 时的原子性

CLH 变体队列

管理等待线程顺序

LockSupport

实现线程阻塞和唤醒

这四个点一起组成了 AQS 的骨架。


26. AQS 常见源码级核心方法

你不用全背,先认脸熟。

独占相关

  • acquire(int arg)

  • release(int arg)

  • tryAcquire(int arg)

  • tryRelease(int arg)

共享相关

  • acquireShared(int arg)

  • releaseShared(int arg)

  • tryAcquireShared(int arg)

  • tryReleaseShared(int arg)

队列相关

  • addWaiter(Node mode)

  • enq(Node node)

  • acquireQueued(Node node, int arg)

  • unparkSuccessor(Node node)

条件队列相关

  • await()

  • signal()

  • signalAll()


27. 为什么说 AQS 是模板方法模式

因为它把主流程写死了:

  • 获取失败怎么办

  • 入队怎么办

  • 挂起怎么办

  • 释放后唤醒谁

这些都由父类 AQS 控制。

子类只负责覆盖几个钩子方法:

  • tryAcquire

  • tryRelease

  • tryAcquireShared

  • tryReleaseShared

这就是非常典型的模板方法。


28. AQS 的一个极简伪代码理解版

获取独占锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final void acquire(int arg) {
if (!tryAcquire(arg)) {
Node node = addWaiter(EXCLUSIVE);
boolean interrupted = false;
for (;;) {
Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
break;
}
if (shouldParkAfterFailedAcquire(p, node)) {
park();
}
if (Thread.interrupted()) {
interrupted = true;
}
}
if (interrupted) selfInterrupt();
}
}

释放独占锁

1
2
3
4
5
6
7
8
9
10
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null) {
unparkSuccessor(h);
}
return true;
}
return false;
}

你看到这里应该有感觉了:

AQS 的世界,其实一直在反复做这件事:

抢资源,失败入队,挂起等待,被唤醒后再抢。


29. 面试最容易问的高频点

AQS 是什么

AQS 是一个同步器框架,通过 state 和 FIFO 等待队列来实现线程同步,很多锁和同步组件都基于它实现。

AQS 的核心组成

  • volatile state

  • CLH 变体双向队列

  • CAS

  • LockSupport

AQS 支持哪两种模式

  • 独占模式

  • 共享模式

AQS 和 ReentrantLock 的关系

ReentrantLock 内部的 Sync 继承 AQS,通过重写 tryAcquire 和 tryRelease 实现可重入互斥锁。

Condition 和 AQS 的关系

ConditionObject 是 AQS 的内部条件队列实现,await 会进入条件队列,signal 会把节点转移到同步队列重新竞争锁。

公平锁和非公平锁区别

公平锁会先检查队列中是否已有前驱节点,非公平锁会直接尝试 CAS 抢占。


30. 你可以直接背的面试版回答

版本一,正常详细版

AQS 全称 AbstractQueuedSynchronizer,是 JUC 中用来构建锁和同步器的基础框架。它的核心思想是用一个 volatile 修饰的 state 表示同步状态,用一个 FIFO 双向等待队列管理获取同步状态失败的线程,并结合 CAS 和 LockSupport 实现原子更新、线程阻塞与唤醒。AQS 支持独占和共享两种模式。像 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都是基于 AQS 实现的。AQS 本身定义了 acquire、release 等模板方法,子类只需要重写 tryAcquire、tryRelease 等方法来自定义资源获取和释放规则。

版本二,简洁版

AQS 就是一个同步器脚手架。它负责排队、阻塞、唤醒和状态管理,子类只负责定义怎样算获取成功、怎样算释放成功。底层核心是 state、CLH 队列、CAS 和 park/unpark。


31. 学 AQS 时最容易晕的几个点

点 1:AQS 不是锁

AQS 是“造锁框架”,锁是它的子类实现。

点 2:state 不是固定表示锁数量

state 是通用同步状态,含义取决于子类。

点 3:入队后不会一直抢

只有接近队头的线程才有资格重点尝试,其他线程会 park。

点 4:signal 不是立刻执行

signal 只是把条件队列的节点转移回同步队列,后面还要重新抢锁。

点 5:共享模式会传播

一个线程成功后,可能继续唤醒后继,让多个线程一起通过。


32. 一张脑内总图

你可以把 AQS 想成这个模型:

├─ state:资源状态
├─ 同步队列:抢资源失败的线程排队
├─ 条件队列:await 的线程暂存
├─ acquire/release:主流程模板
├─ tryAcquire/tryRelease:子类自定义规则
├─ CAS:改状态、入队时保证原子性
└─ park/unpark:线程挂起与唤醒


33. 你当前阶段最该掌握的主线

Gilbert,现阶段你先把这 6 句拿稳,就已经很能打了:

  1. AQS 是同步器框架,不是具体锁

  2. 核心变量是 volatile state

  3. 抢不到资源的线程会进入 FIFO 双向队列

  4. 独占模式常见于 ReentrantLock,共享模式常见于 Semaphore、CountDownLatch

  5. 底层依赖 CAS + CLH 队列 + LockSupport

  6. Condition 有独立条件队列,被 signal 后会回到同步队列重新抢锁