概述

AQS 是 Java 并发包中构建锁和同步器的基础框架。
- 全称:
AbstractQueuedSynchronizer
可以把它理解成:AQS 使用一个同步状态 state、一个等待队列以及 CAS,统一解决“资源竞争失败后,线程如何排队、阻塞、唤醒和重新竞争”的问题。
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都建立在 AQS 之上。
解决什么问题
只有 CAS 时,可以这样竞争资源:
while (!compareAndSetState(0, 1)) {
// 不断重试
}
竞争激烈时,失败线程会一直自旋,浪费 CPU(即自旋锁)。
更合理的方式是:
尝试获取资源
↓
获取成功:继续执行
↓
获取失败:进入等待队列并阻塞
↓
资源释放:唤醒等待线程
↓
被唤醒后:重新竞争资源
这就是 AQS 处理的核心流程:先尝试获取,失败后排队,并在必要时挂起线程。
AQS 将通用流程封装起来:
- 使用 CAS 修改同步状态
- 维护等待线程队列
- 挂起线程
- 唤醒后继线程
- 处理取消和中断
- 支持独占模式与共享模式
具体同步器只需要定义:
- 什么情况算获取成功
- 获取时如何修改
state - 释放时如何修改
state
核心组成
同步状态 state
AQS 内部维护一个整数同步状态:
- 它的具体含义由子类决定
private volatile int state;
在 ReentrantLock 中:
state == 0 锁没有被持有
state == 1 当前线程持有一次
state == 2 当前线程重入两次
在 Semaphore 中:
state = 当前剩余许可证数量
因此,state 没有统一的业务含义。它只是 AQS 提供的一块同步状态存储空间。
AQS 提供了一些修改方法:
protected final int getState();
protected final void setState(int newState);
protected final boolean compareAndSetState(
int expect,
int update
);
同步等待队列
当线程获取同步资源失败时,AQS 会将其加入同步等待队列。节点按入队关系组织,但最终获取顺序还取决于子类的获取策略;使用 AQS 并不自动意味着严格公平。
逻辑上可以看成是双向链表:
head表示当前已经获得同步资格的节点;真正等待的线程一般位于head后面- 节点成功获取资源后,会成为新的
head
获取和释放模板
AQS 将同步操作分成两种模式。
独占模式:同一时刻通常只有一个线程成功。
- 典型实现:
ReentrantLock - 核心入口:
acquire()、release()
共享模式:同一时刻可能允许多个线程成功。
- 典型实现:
Semaphore - 核心入口:
acquireShared()、releaseShared()
模板方法设计
AQS 定义了完整的排队、阻塞和唤醒流程,但把资源判断逻辑留给子类实现。
子类主要覆盖以下方法:
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
protected boolean isHeldExclusively();
AQS 对外的通用流程类似:
public final void acquire(int arg) {
if (!tryAcquire(arg)) {
// 加入队列、阻塞、唤醒后重试
}
}
这里体现了职责分离:
tryAcquire()
子类决定是否成功
入队、阻塞、唤醒、重试
AQS 统一完成
独占模式获取资源
以 ReentrantLock 的 lock() 为例,该方法最终会进入类似:
acquire(1);
完整流程:
调用 acquire(1)
↓
调用子类 tryAcquire(1)
↓
是否获取成功?
├─ 成功:直接返回
└─ 失败:创建等待节点
↓
加入同步队列
↓
循环检查前驱
↓
满足条件时再次 tryAcquire
↓
仍失败则 park
↓
被唤醒后继续循环
第一次尝试获取锁:通过 CAS 修改 state
- 成功后把持有线程记录在同步器继承的所有权字段中,而不是 Java 对象头中
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(currentThread);
return true;
}
如果锁已经被持有,那需要判断是否重入:
- 增加
state
if (currentThread == getExclusiveOwnerThread()) {
int nextState = state + acquires;
setState(nextState);
return true;
}
如果锁被其他线程持有,即 线程 B tryAcquire() 失败,AQS 会为线程 B 创建节点并加入同步队列。
假设队列原来是:
head → 节点 C ← tail
线程 B 加入后:
- 这里的尾节点更新依旧使用 CAS
head → 节点 C → 节点 B ← tail
进入同步队列后并不会立刻阻塞,而是进入一个循环:
检查自己的前驱节点
↓
如果前驱是 head
↓
再次尝试获取资源
在典型的独占获取流程中,通常由 head 的后继节点再次尝试获取资源,从而避免所有等待线程同时竞争。
线程阻塞
AQS 底层主要通过以下两个方法来阻塞和唤醒线程:
LockSupport.park();
LockSupport.unpark(thread);
park
暂停当前线程:
LockSupport.park(this);
线程会停止继续占用 CPU,直到发生以下情况之一:
- 其他线程调用
unpark() - 当前线程被中断
park()无原因返回,也就是允许的虚假返回
park() 返回不代表线程已经获得锁,只代表:线程可以醒来,再次检查同步状态。
- 这点和 Java Condition 类似:醒来后依然要检查条件是否满足
因此 AQS 必须使用循环:
for (;;) {
if (tryAcquire(arg)) {
return;
}
park();
}
unpark
释放锁时,AQS 会找到合适的后继节点:
LockSupport.unpark(successor.thread);
被唤醒线程从 park() 返回,然后重新执行:
tryAcquire(arg);
如果仍然失败,它可能再次进入阻塞。