Java AQS(AbstractQueuedSynchronizer)是 Java 并发包(java.util.concurrent)的核心基石,它通过一个核心变量和两种队列,完美实现了锁机制与线程通信。
AQS 核心要素
AQS 的架构设计极其精巧,主要建立在以下三大核心要素之上:
- 状态变量(
volatile int state):表达锁或同步资源的状态。 - 同步队列(双向 CLH 队列):负责管理多线程对锁资源的竞争、排队与释放。
- 等待队列(单向 Condition 队列):负责管理线程间的条件等待与唤醒通信。
两大核心能力
锁机制(同步队列)
同步队列是一个双向 FIFO 链表,用于管理多线程对资源的排队与竞争:
- 功能:管理多线程对锁的竞争与释放。
- 排队:抢锁失败的线程会被封装为 Node 节点加入双向 CLH 队列尾部并阻塞(
LockSupport.park())。 - 类型:
- 独占锁:同一时刻仅允许一个线程获取锁(如
ReentrantLock)。 - 共享锁:允许多个线程同时获取许可(如
Semaphore、CountDownLatch)。
- 独占锁:同一时刻仅允许一个线程获取锁(如
线程通信(ConditionObject)
ConditionObject 是 AQS 内部实现了 Condition 接口的内建类,用于替代传统的 Object.wait() 和 Object.notify():
- 功能:实现精准的线程间条件等待与通知。
- 精准通知:一个 Lock 锁可以创建多个
Condition实例(即关联多个独立的等待队列),比传统单等待池更加灵活精准。 - 节点转移:当调用
signal()时,AQS 会将节点从 Condition 单向等待队列的首部摘下,并通过 CAS 转移回 CLH 双向同步队列中重抢锁。
协同工作流程
同步队列与等待队列在线程抢锁与条件等待时的完整协同逻辑如下:
- 抢锁失败 $\rightarrow$ 进入双向同步队列排队阻塞。
- 拿到锁但条件不满足 $\rightarrow$ 调用
await()释放锁所有权(state清零),并进入单向 Condition 等待队列挂起。 - 被其他线程唤醒 $\rightarrow$ 其他持锁线程调用
signal(),将节点从 Condition 等待队列移回双向同步队列重新争抢锁。
核心数据结构源码
Node 节点定义
在 JDK 8+ 的 HotSpot 实现中,AQS 内部节点定义如下:
abstract static class Node {
volatile Node prev; // 双向同步队列前驱节点
volatile Node next; // 双向同步队列后继节点
Thread waiter; // 当前处于等待状态的线程
volatile int status; // 节点状态 (SIGNAL / CANCELLED / CONDITION / PROPAGATE)
Node nextWaiter; // 单向 Condition 等待队列后继节点
}
节点的 status 状态枚举决定了线程在队列中的行为:
SIGNAL(-1):后继节点的线程处于挂起状态,当前节点释放锁或取消时必须主动唤醒后继节点。CANCELLED(1):由于超时或响应中断,当前节点的线程获取锁请求已被取消,需要从链表中剔除。CONDITION(-2):当前节点正处于 Condition 条件队列中等待。PROPAGATE(-3):共享模式下,唤醒动作需要向后续节点无条件传播。
CLH 变体双向队列
CLH 队列(Craig, Landin, and Hagersten 队列)是一种基于链表的自旋锁队列,AQS 的同步队列是基于 CLH 队列的改进变体。
特点与改进:
- 虚链表结构:AQS 维持一个由
head和tail指针控制的双向队列,节点代表等待获取锁的线程。 - 自旋与 park 结合:CLH 原生是自旋死等,而 AQS 在竞争失败后让线程自旋几次后调用
LockSupport.park()休眠,极大节省 CPU。 - 双向指针支持取消:增加
prev和next双向指针,使得节点在发生取消(CANCELLED)时能够快速从队列中自愈摘除。
源码实现:获取与释放
AQS 采用模板方法设计模式,子类只需实现 tryAcquire / tryRelease(独占模式)或 tryAcquireShared / tryReleaseShared(共享模式)。
独占式获取锁 (acquire)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 流程要点:
- 尝试调用子类重写的
tryAcquire(arg)通过 CAS 更新state。 - 若获取失败,调用
addWaiter将当前线程封装为独占 Node 入队尾(通过 CAS 保证入队并发安全)。 - 执行
acquireQueued:自旋检查前驱节点是否为head。如果是head,再次尝试tryAcquire;若不是或再次获取失败,检查前驱节点status是否为SIGNAL,若是则调用LockSupport.park()挂起等待。
- 尝试调用子类重写的
独占式释放锁与头节点唤醒 (release)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.status != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- 唤醒流程 (
unparkSuccessor):- 当持锁线程调用
release成功释放资源后,AQS 检查head节点。 - 若
head != null且status != 0,调用unparkSuccessor(head)。 unparkSuccessor会找head.next节点;若后继节点为空或已被取消(status > 0),则从队尾tail反向往前查找离head最近的有效节点,调用LockSupport.unpark(s.waiter)唤醒。
- 当持锁线程调用
总结:
- 虚头节点(
head)本身不代表实际线程,仅作为唤醒后继的锚点。- 唤醒操作始终优先针对头节点的下一个有效节点,保证了 FIFO 的排队秩序与公平性。
共享模式与传播唤醒 (acquireShared / releaseShared)
在共享模式下(如 Semaphore 释放许可证、CountDownLatch 归零),锁可以同时被多个线程持有。
- 传播唤醒 (
PROPAGATE):- 当一个线程成功获取共享锁(
tryAcquireShared > 0)后,除了自己拿到资源,还会调用setHeadAndPropagate。 - 该方法会检查后续节点是否也是共享模式;如果是,会主动沿着队列链表继续唤醒下一个节点(
doReleaseShared),形成“唤醒传播”,使得多个等待线程可以并发解挂。
- 当一个线程成功获取共享锁(
Condition 条件队列机制
ConditionObject 是 AQS 内部实现的条件等待队列(单向链表):
await()核心流程:- 当前线程调用
condition.await()。 - 线程将自身包装为
status = Node.CONDITION的节点,加入当前 Condition 对象的单向等待队列尾部。 - 完全释放当前持有的锁(调用
fullyRelease()归零state,并唤醒同步队列中的后继节点)。 - 调用
LockSupport.park()挂起自身,直到被signal()或中断。
- 当前线程调用
signal()核心流程:- 持锁线程调用
condition.signal()。 - AQS 从 Condition 单向链表头部弹出第一个未取消的节点
firstWaiter。 - 调用
transferForSignal通过 CAS 将节点status重置为0,并挂载到双向 CLH 同步队列尾部。 - 将该节点在同步队列中前驱节点的
status设置为Node.SIGNAL,以便后续获得唤醒通知。
- 持锁线程调用
经典应用实现解析
AQS 同步队列模式对比
| 同步组件 | AQS 模式 | state 变量的物理含义 | 核心机制 |
|---|---|---|---|
ReentrantLock | 独占 | 表示锁的重入次数(0 代表无锁,$N$ 代表重入次数) | 可重入锁;支持公平/非公平获取策略 |
Semaphore | 共享 | 表示可用资源的许可证数量 | acquire() 递减 state,release() 递增 state |
CountDownLatch | 共享 | 表示未完成的并行任务倒计数 | countDown() 递减 state,归零时触发 doReleaseShared |
ReentrantReadWriteLock | 混合 | 高 16 位表示读锁重入次数,低 16 位表示写锁重入次数 | 读写分离,支持锁降级 |
使用 Condition 条件队列的经典类实现
在 Java 官方 SDK(JUC 包)中,使用 ConditionObject(Condition 条件队列)的经典类主要集中在阻塞队列与线程池协同场景:
阻塞队列系列(BlockingQueue)与线程池 Worker 协同
在 Java 的 ThreadPoolExecutor 中,核心工作线程(Worker)去任务队列(workQueue)拉取任务时,逻辑并不是“一次只能有一个线程来拉取”,而是:“当队列为空时,所有消费线程都要挂起等待;一旦有新任务入队,再唤醒线程去消费”。底层调用的正是基于 Condition Queue(条件队列) 实现的阻塞队列(如 ArrayBlockingQueue 或 LinkedBlockingQueue)。
核心场景:workQueue.take()
以 ArrayBlockingQueue 为例,其底层由一把独占锁 ReentrantLock + 一个条件队列 notEmpty(Condition)实现。
线程池中的 Worker 线程获取任务的伪代码逻辑如下:
// ThreadPoolExecutor 内部 Worker 线程获取任务的伪代码逻辑
Runnable getTask() {
// 调用阻塞队列的 take() 方法拉取任务
// 如果队列为空,线程就会在这里被挂起(进入 notEmpty 的 Condition Queue)
Runnable task = workQueue.take();
return task;
}
底层源码逻辑:take() 与 put()
ArrayBlockingQueue 的 take()(消费拉取)与 put()(生产提交)源码逻辑:
消费线程(Worker)拉取任务:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 1. 先抢锁,保证并发安全
try {
// 2. 如果队列为空,不能消费!
while (count == 0) {
// 3. 线程进入 notEmpty 条件队列挂起,并释放锁
notEmpty.await();
}
// 4. 队列不为空了,出队消费
return dequeue();
} finally {
lock.unlock(); // 5. 释放锁
}
}
生产者(外部线程)提交任务:
public void put(E e) throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
notFull.await(); // 队列满了,生产者挂起
}
enqueue(e); // 放入任务
// 【关键点】放入任务后,通知在 notEmpty 条件队列里等待的消费线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
为什么条件等待必须使用 while 而非 if?(虚假唤醒 Spurious Wakeup)
在 take() 和 put() 源码中,条件检查均使用了 while 循环(如 while (count == 0) notEmpty.await();),而严禁使用 if 条件分支。其核心原因之一正是为了应对 虚假唤醒(Spurious Wakeup) 以及多线程并发竞态。
什么是虚假唤醒(Spurious Wakeup)?
- 线程在没有接收到任何显式通知(
signal()/signalAll())、未超时且未被中断的情况下,操作系统内核调度机制也可能使其从挂起(等待)状态中苏醒过来。 - 醒来时,线程所等待的业务条件(如
count > 0)实际上并不满足。
- 线程在没有接收到任何显式通知(
为什么底层操作系统/内核会发生虚假唤醒?
- 内核调度与并发性能权衡:在操作系统(如 Linux POSIX 线程库
pthread_cond_wait)的实现中,为了保证多核 CPU 下极致的并发调度吞吐量,内核在调度中断、信号交付(Signal Delivery)、上下文切换或系统调用重新启动时,允许发生极小概率的假性唤醒。 - 避免昂贵的内核级同步开销:如果要求内核在唤醒线程的瞬间 100% 绝对保证条件成立,需要在内核层施加极其沉重的跨核同步锁与状态双重检查,这会严重拖慢多核并发调度的吞吐性能。因此,POSIX 标准和操作系统将“重新验证条件”的职责下放给了用户态应用程序。
- 内核调度与并发性能权衡:在操作系统(如 Linux POSIX 线程库
导致醒来后条件不满足的三大典型场景:
- 操作系统级虚假唤醒:内核调度或信号扰动直接将等待线程唤醒,此时队列依然为空(
count == 0)。 signalAll()广播竞争:生产者放入 1 个数据后调用了signalAll(),唤醒了多个等待中的消费线程。第 1 个抢到锁的线程消费了该数据(count变回 0),后续醒来的线程若用if会继续执行dequeue()导致越界或空指针,用while则重新检查并安全回退挂起。- 持锁抢占与插队(Barging):在等待线程被
signal()唤醒从条件队列移入同步队列排队、但尚未真正抢到锁的间隙,刚好有新来的消费线程以非公平方式先一步抢到了锁并消费了数据。
- 操作系统级虚假唤醒:内核调度或信号扰动直接将等待线程唤醒,此时队列依然为空(
编程规范与不变式: 无论是使用 AQS 的
Condition.await(),还是底层的Object.wait()或 POSIXpthread_cond_wait,永远必须在while循环中检查等待条件,这是并发编程中绝对的黄金法则:while (!conditionSatisfied()) { condition.await(); }
完整协同过程演示
队列为空,线程挂起:
- 线程池里的 3 个 Worker 线程同时去
take()任务。 - 线程 A 抢到了锁,发现
count == 0,调用notEmpty.await(),锁被释放,线程 A 被封装为 Node 进入notEmpty的 Condition Queue 挂起。 - 线程 B、C 依次抢到锁,同样发现
count == 0,也都调用notEmpty.await()进入 Condition Queue。 - 此时,3 个 Worker 线程都在 Condition Queue 里休眠。
- 线程池里的 3 个 Worker 线程同时去
生产任务,精准唤醒:
- 外部提交了一个新任务
submit(task),生产者调用put()。 - 生产者把任务放入队列后,执行
notEmpty.signal()。 - Condition Queue 头的线程 A 被摘下,转移回 AQS 的双向同步队列(CLH 队列)去竞争锁。
- 线程 A 抢到锁后从
await()返回,退出while (count == 0)循环,成功拿到任务去执行!
- 外部提交了一个新任务
线程协同总结
互斥(一次只有一个线程操作队列):靠
ReentrantLock的 AQS 双向同步队列 实现。等待/通知(队列为空时挂起,有任务时唤醒):靠
notEmpty的 Condition Queue(单向条件队列) 实现。ArrayBlockingQueue(单锁双 Condition 条件队列):- 内部维护一把独占锁
ReentrantLock lock,以及两个 Condition 条件队列:notEmpty(非空条件队列)与notFull(非满条件队列)。 - 双 Condition 的意义:实现了生产者与消费者的精准定向唤醒,避免了传统
Object.notifyAll()唤醒错误线程组带来的 CPU 无谓浪费。
- 内部维护一把独占锁
LinkedBlockingQueue(双锁双 Condition 条件队列):- 内部维护两把独立的锁
takeLock和putLock,分别搭配两个 Condition:takeLock搭配notEmptyCondition 队列,putLock搭配notFullCondition 队列。 - 出队和入队各自使用独立的锁与 Condition 队列,实现了并发读写彻底解耦。
- 内部维护两把独立的锁
DelayQueue&PriorityBlockingQueue(优先级/延迟阻塞队列):- 内部使用
ReentrantLock搭配availableCondition 队列。当队头元素尚未到达延迟时间时,消费者线程在available条件队列上调用awaitNanos()超时挂起。
- 内部使用
线程池与执行器(ThreadPoolExecutor)
- 工作线程任务拉取:线程池的核心 Worker 线程在
getTask()从任务队列拉取任务时,底层调用的就是workQueue.take()或workQueue.poll(keepAliveTime),本质上就是挂起在阻塞队列的 Condition 条件队列上。 awaitTermination()优雅等待:调用ThreadPoolExecutor.awaitTermination(timeout, unit)时,主线程挂起在内部terminationCondition 条件队列上。当线程池彻底变为TERMINATED状态时,调用termination.signalAll()唤醒所有等待关闭的主线程。
Condition 条件队列应用对比表
| 经典类 / 组件 | 关联的 Condition 数量 | Condition 队列的物理用途 | 唤醒协同方式 |
|---|---|---|---|
ArrayBlockingQueue | 2 个(notEmpty, notFull) | 精准区分“队列空”与“队列满”挂起 | put() 唤醒 notEmpty;take() 唤醒 notFull |
LinkedBlockingQueue | 2 个(notEmpty, notFull) | 配合 takeLock / putLock 实现并发读写解耦 | 读写双锁分别管理各自的 Condition 队列 |
DelayQueue | 1 个(available) | 队头元素未到期时,消费者超时挂起等待 | take() 定时挂起;put()/队头变更 signal() |
ThreadPoolExecutor | 1 个(termination) | 外部主线程等待线程池彻底终止 | 彻底 termination 时 signalAll() 唤醒 |
CyclicBarrier | 1 个(内部 trip Condition) | 等待全员到达屏障点时所有线程挂起 | 最后一个线程到达时 trip.signalAll() 解挂全员 |