AQS 独占锁原理详解
深入理解 Java 并发包中 AbstractQueuedSynchronizer 的独占锁实现原理
AQS
AbstractQueuedSynchronizer (AQS) 是 Java 并发包 (java.util.concurrent) 的 核心基石。
简单来说,AQS 是一个用来构建锁和同步器的 框架,JDK 中的 ReentrantLock、CountDownLatch、Semaphore 等,底层全部依赖 AQS 来管理线程的排队、阻塞和唤醒。
一、 核心原理
AQS 的核心思想可以概括为:状态 (State) + 队列 (Queue) + CAS。
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争抢资源被阻塞时进入此队列)。
1. 核心要素
- 资源状态 (
volatile int state):- AQS 使用一个
int类型的成员变量state来表示同步状态。 - 使用
volatile保证可见性。 - 使用 CAS (Compare-And-Swap) 对
state进行原子修改,保证线程安全。
- AQS 使用一个
- CLH 等待队列:
- 当线程获取资源(锁)失败时,AQS 会将该线程封装成一个 Node 节点。
- 这个 Node 会被加入到一个双向链表中(CLH 锁队列的变体)。
- 队列遵循 FIFO(先进先出)原则。
- 阻塞与唤醒:
- AQS 依赖
LockSupport.park()来阻塞线程(挂起)。 - 依赖
LockSupport.unpark()来唤醒线程。
- AQS 依赖
2. 两种资源共享方式
AQS 定义了两种资源共享方式,子类根据需要实现:
- Exclusive (独占模式): 只有一个线程能执行,如
ReentrantLock。 - Shared (共享模式): 多个线程可同时执行,如
Semaphore、CountDownLatch。
3. 模板方法模式 (Template Method Pattern)
AQS 使用了模板方法设计模式。AQS 负责把“排队”、“阻塞”、“唤醒”这些复杂的脏活累活都做好了,使用者(子类)只需要重写以下几个简单的方法来定义“什么叫获取成功,什么叫获取失败”:
tryAcquire(int): 独占方式尝试获取资源。tryRelease(int): 独占方式尝试释放资源。tryAcquireShared(int): 共享方式尝试获取资源。tryReleaseShared(int): 共享方式尝试释放资源。isHeldExclusively(): 当前线程是否独占资源。
二、 工作流程
以 ReentrantLock (非公平锁) 为例,AQS 的工作流程如下:
- 尝试获取: 线程 A 调用
lock(),底层通过 CAS 尝试将state从 0 改为 1。 - 成功: 如果成功,设置当前线程为独占线程,直接执行业务代码。
- 失败 (入队):
- 如果线程 B 来了,发现
state已经是 1 了,CAS 失败。 - 线程 B 被封装成 Node 节点,追加到 CLH 队列的尾部(tail)。
- 如果线程 B 来了,发现
- 阻塞: 线程 B 在队列中自旋检查前驱节点是否是头节点,如果拿不到锁,就调用
LockSupport.park()让自己沉睡。 - 释放与唤醒:
- 线程 A 业务做完,调用
unlock()。 state减为 0。- AQS 唤醒队列头部节点的后继节点(即线程 B)。
- 线程 B 醒来,再次尝试 CAS 获取锁。
- 线程 A 业务做完,调用
三、 实际应用
AQS 在 JDK 中应用极其广泛,不同的同步器对 state 有不同的定义:
| 同步组件 | 模式 | state 的含义 | 原理简述 |
|---|---|---|---|
| ReentrantLock | 独占 | 锁的持有次数 | state=0 表示无锁;state>0 表示重入次数。释放时需减到 0 才是真正释放。 |
| CountDownLatch | 共享 | 剩余计数值 | 初始化 state=N。countDown() 也是 CAS 将 state 减 1。await() 检查如果 state!=0 就入队阻塞。 |
| Semaphore | 共享 | 剩余许可数量 | state 代表信号量个数。acquire() 时 state 减 1,release() 时 state 加 1。 |
| ReentrantReadWriteLock | 混合 | 高 16 位/低 16 位 | state 变量被拆分:高 16 位表示 读锁(共享),低 16 位表示 写锁(独占)。 |
四、概括
AQS 是 Java 并发包中的一个抽象同步框架,核心作用是统一封装线程等待、唤醒和排队机制。
底层用一个 state 变量 + FIFO 等待队列( CLH 变体队列)实现了线程资源的安全抢夺。
volatile state 变量:表示资源状态(比如 0 = 未锁定,1 = 已锁定);
FIFO 等待队列:基于双向链表实现的变体 CLH 队列,每个节点记录线程信息和等待状态;
线程抢不到锁就被加入队列中去排队,AQS 不负责具体锁逻辑(如加锁 / 释放),仅处理线程排队细节,具体锁语义由实现类(如 ReentrantLock、CountDownLatch)自行实现。
AQS 是 Java 并发的 发动机,理解了 AQS,就理解了为什么 Java 的锁比 synchronized 更加灵活(支持超时、中断、公平/非公平)。
- 关键点:
volatile state维护状态,CAS保证原子性,CLH 队列管理排队,LockSupport控制阻塞。
ReentrantLock 示例解析
NonfairSync(非公平锁)是 ReentrantLock 的默认实现,也是我们在绝大多数场景下推荐使用的模式。
它的核心理念是:极度的实用主义,它并不保证先来后到,而是认为“如果刚好有线程来请求锁,而锁刚好可用,那就直接给它,不用去排队唤醒睡着的线程了”。这能极大地减少线程上下文切换的开销。
下面我们深入源码,看它是如何通过两次“插队”来实现高性能的。
一、核心入口方法
这是非公平锁最“野蛮”的地方。当一个线程调用 lock() 时,它不会像公平锁那样先去问 AQS 队列里有没有人在排队,而是直接尝试抢占。
// ReentrantLock.NonfairSync 中的实现
final void lock() {
// 1. 第一次插队:直接 CAS 抢锁
// 也就是常说的:不管三七二十一,先抢一下试试
if (compareAndSetState(0, 1))
// 抢到了,将当前线程设为持有者
setExclusiveOwnerThread(Thread.currentThread());
else
// 没抢到,老老实实走 AQS 的标准获取流程
acquire(1);
}
- 对比 FairSync: 公平锁的
lock()方法里没有这个if判断,它直接调用acquire(1),完全遵循排队规则。 - 如果锁刚好被释放,新来的线程不需要经历“入队 -> 阻塞 -> 唤醒”的昂贵过程,直接就能拿到锁执行,这就是插队。
二、AQS 回调
当 lock() 中的第一次 CAS 失败后,代码进入 acquire(1),AQS 的 acquire 模板方法会调用子类实现的 tryAcquire。
在 NonfairSync 中,tryAcquire 直接调用了 nonfairTryAcquire。
这里发生了 第二次插队。
// ReentrantLock.Sync (父类中定义,NonfairSync 直接调用)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前锁的状态
// --- 场景 1: 锁当前是空闲的 (state == 0) ---
if (c == 0) {
// 2. 第二次插队
// 即使队列里有人在排队,我也不管,我只要能 CAS 成功,这锁就是我的
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// --- 场景 2: 锁已经被持有了,但持有者是自己 (重入逻辑) ---
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 增加重入次数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 重点:这里不需要 CAS!
// 因为已经持有锁了,其他线程进不来,没有并发竞争,直接赋值即可
setState(nextc);
return true;
}
// --- 场景 3: 锁被别人拿着 ---
return false; // 返回 false,AQS 会把当前线程通过 addWaiter 加入队列
}
关键源码细节解析:
- 为什么叫 Nonfair (非公平)?
- 在
c == 0的判断块中,它 没有 去检查 AQS 队列中是否有前驱节点(即没有调用hasQueuedPredecessors())。 - 只要当前锁是空的,它就尝试 CAS。这意味着:一个刚刚到达的线程,可能会抢在已经在队列里等了很久的线程前面拿到锁。
- 在
- 重入 (Reentrancy) 的实现
- 通过
current == getExclusiveOwnerThread()判断。 - 重入仅仅是
state + 1。 - 释放锁时,必须
state减到 0 才会真正释放给其他线程。
- 通过
三、对比 FairSync (公平锁)
为了更清晰地理解 NonfairSync 的“流氓”行径,看一眼 FairSync 的 tryAcquire 区别:
// FairSync 的实现
protected final boolean tryAcquire(int acquires) {
// ... 省略部分代码 ...
if (c == 0) {
// ! hasQueuedPredecessors() 是核心区别!
// 只有当“队列为空”或者“我是队列里的第一个”时,才允许 CAS
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...
}
- FairSync: 讲究先来后到,不仅看
state,还要看队列。 - NonfairSync: 讲究效率优先,只看
state,不看队列。
四、为什么要“非公平”?
你可能会问,非公平锁会不会导致队列里的线程一直拿不到锁(饥饿现象)?
理论上会,但实际中极少发生,Java 选择非公平锁作为默认值,主要基于性能考量:
-
减少唤醒开销: 恢复一个被挂起(Parked)的线程需要从内核态切回用户态,这个过程很慢。
-
利用时间差: 假设线程 A 释放锁,唤醒线程 B,在 B 还没完全醒过来并被调度执行之前,线程 C 刚好请求锁。
-
非公平模式: C 抢到了锁,迅速执行完并释放,此时 B 刚好醒过来,拿到锁。这样 A、B、C 都没有浪费 CPU 时间,吞吐量极高。
-
公平模式: C 只能入队等待,CPU 在 B 醒来之前可能处于空闲状态,浪费了算力。
-
源码核心:
NonfairSync 在 lock() 入口处和 tryAcquire() 逻辑中,两次 忽略队列情况直接尝试 CAS 修改 state,只有这两次都失败了,才会乖乖去 AQS 队列里排队。
五、动画演示
下面用 ReentrantLock 的 非公平锁 的动画展示了 AQS 独占锁的完整竞争过程
Tip
这里以 Thread A、B、C 三个线程的锁竞争、入队、阻塞和唤醒流程作为演示,你可以清晰地看到整个锁竞争的过程,包括队列的懒加载初始化、节点的 waitStatus 变化、以及线程的阻塞与唤醒机制
AQS 独占锁竞争全过程
此时内存中没有任何 Node,队列是 懒加载 (Lazy Loading) 的。
Condition 示例解析
Condition (条件队列) 是 AQS 中非常精彩的一部分,它配合锁实现了线程间的 精确等待与唤醒。
如果说 state 和 AQS 队列解决了“怎么抢锁”的问题,那么 Condition 就解决了“抢到锁了,但条件不满足,怎么先歇会儿(释放锁)等条件满足了再回来”的问题。
它是 Object.wait() / Object.notify() 的升级版,最大的优势在于 支持多个等待队列(One Lock, Multiple Conditions)。
一、核心概念
理解 Condition 的关键在于脑海中要有 两个队列 的概念:
- 同步队列 (Sync Queue / AQS Queue): 也就是前面提到的 CLH 队列,这里的线程都在 排队抢锁。
- 条件队列 (Condition Queue): 这是
ConditionObject内部维护的一个单向链表,这里的线程都在 等待某个条件(比如队列不满、金额足够),它们 不持有锁,也不在抢锁,而是处于“挂起”状态。
Condition 机制的本质,就是节点在这两个队列之间的“大迁徙”。
二、核心结构
ConditionObject 是 AQS 的内部类。
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter; // 条件队列的头节点
private transient Node lastWaiter; // 条件队列的尾节点
// ...
}
- 复用 Node: 它复用了 AQS 的
Node类,但使用的是nextWaiter属性将节点串成单向链表。 - 状态标记: 处于条件队列中的节点,其
waitStatus被标记为Node.CONDITION(-2)。
三、核心流程源码解析
流程图

我们以最经典的“生产者-消费者”场景为例:线程 A 调用 await() 等待,线程 B 调用 signal() 唤醒。
1. await():从“同步队列”迁移到“条件队列”
当线程 A 拿到锁后,发现条件不满足(比如队列满了,我是生产者),调用 condition.await()。
public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
// 1.把自己封装成 Node,加入到【条件队列】的队尾
Node node = addConditionWaiter();
// 2.【彻底释放锁】
// 注意:await 是会释放锁的,而且是 fullyRelease(不管重入了多少次,一次性减到 0)
// 返回值 savedState 是为了将来被唤醒重新拿锁时,能恢复重入次数
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 阻塞挂起
// 只要该节点还在条件队列中(没被移动到 Sync 队列),就一直 park 睡大觉
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 被 signal 唤醒后,进入 acquireQueued 重新在【同步队列】里抢锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// ...
}
关键动作: 持有锁的线程 -> 构造节点入条件队列 -> 释放锁 -> LockSupport.park()。
2. signal():从“条件队列”迁移回“同步队列”
线程 B 拿到锁,消费了一个数据,队列不满了,调用 condition.signal() 通知线程 A。
public final void signal() {
// 必须持有独占锁才能 signal,否则抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 唤醒条件队列里的第一个家伙
}
// 核心逻辑在 doSignal 调用的 transferForSignal 中
final boolean transferForSignal(Node node) {
// 1. 修改状态:从 CONDITION (-2) 改为 0 (初始状态)
// 如果失败说明该节点可能已经取消等待了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 2. 【关键一步】:将节点从【条件队列】转移到【同步队列】(enq)
// p 是该节点在同步队列中的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 3. 如果前驱节点取消了,或者尝试设置前驱节点为 SIGNAL 失败
// 则直接 unpark 唤醒当前线程,让它去 acquireQueued 里自旋修正
// 通常情况下,这里只做转移,不立即唤醒,等线程 B unlock 的时候自然会唤醒 A
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
关键动作: 摘下条件队列头节点 -> 修改状态 -> enq() 插入同步队列尾部。
Warning
注意: signal() 并不等于“立刻唤醒线程运行”,它只是把线程从“等待室”(条件队列)领到了“候诊室”(同步队列)。
线程 A 此时还在排队,只有等线程 B 调用 unlock() 真正释放锁后,A 才有机会抢到锁并从 await() 中返回。
四、动画演示
同样的,这里再提供一个动画,你可以一步一步感受线程是如何在 AQS 的同步队列与条件队列之间互相流转的。
Tip
推荐这样操作:
- 先点击 “加锁” 按钮,进入抢锁流程
- 再点击 “await” 按钮,我这里会同时加入新的线程并抢到锁,观察线程的变化
- 可以多点击两三次 “await” 按钮,多让几个线程都进入条件队列中,方便观看
- 后可以再多点击两三次 “signal” 按钮,让线程从条件队列中出来,进入同步队列中
- 最后点击 “unlock” 按钮,让当前线程释放锁,同步队列的其他线程去抢锁
AQS 核心机制
AbstractQueuedSynchronizer
State Engine
同步队列 Sync Queue
条件队列 Condition Queue
public final void await() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
}
}
public final void signal() {
if (firstWaiter != null)
doSignal(firstWaiter);
}
final boolean transferForSignal(Node n) {
// 1. WS: -2 -> 0
casWaitStatus(n, -2, 0);
// 2. Enq Sync Queue
enq(n);
}系统日志 Log
五、实际应用 ArrayBlockingQueue
JDK 中的阻塞队列 ArrayBlockingQueue 是 Condition 最教科书式的应用,它使用了 一把锁 + 两个条件:
final ReentrantLock lock;
final Condition notEmpty; // 消费者等待队列:没得吃了,等不空
final Condition notFull; // 生产者等待队列:吃撑了,等不满
public void put(E e) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 满了,在 notFull 条件上等待
enqueue(e);
// 生产完了一个,队列肯定不空了,喊醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 空了,在 notEmpty 条件上等待
E x = dequeue();
// 消费完了一个,队列肯定不满了,喊醒生产者
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
六、总结
- 数据结构:
Condition内部维护一个单向链表。 - await: 释放锁 -> 进条件队列 -> 阻塞。
- signal: 出条件队列 -> 进同步队列(AQS 队列) -> 准备抢锁。
- 优势: 相比
Object.wait/notify只能混在一个等待池里,Condition 可以将等待线程按“原因”分组(比如notFull和notEmpty),从而避免“惊群效应”或“唤醒了错误的线程”,效率更高。
Tip
小问题思考:
为什么 await() 方法中,调用 addConditionWaiter() 加入条件队列不需要 CAS,而 ReentrantLock 入 AQS 队列 需要 CAS 呢?
因为调用 await 时线程肯定已经持有锁了,是线程安全的;而入 AQS 队列时线程还没拿到锁,存在竞争。