Java AQS核心源码解析:同步器架构与锁获取机制
AQS (AbstractQueuedSynchronizer) 是 JDK 中 ReentrantLock、CountDownLatch 等同步组件的底层基石。理解其内部的状态流转、双向链表结构以及加锁/唤醒机制,是深入掌握 Java 并发编程的关键。
1、整体架构设计
AQS 本质上是锁框架的基础设施,它定义了一套标准的获取锁和释放锁的逻辑模板。任何想要实现自定义同步器的类,只需继承 AQS 并实现少量抽象方法即可。
该架构图展示了 AQS 的核心组成及不同场景下的动态流向:
- 双队列结构: AQS 维护了两个队列,分别是同步队列(CLH变体)用于管理等待获取锁的线程,以及条件队列用于配合 Condition 对象进行线程间通信。两者底层均为双向链表。
- 运行场景: 图中不同颜色的线条代表了排队、尝试获取锁、阻塞、被唤醒等不同阶段的流转逻辑。
1.1、核心属性与数据结构
要理解 AQS 的工作原理,首先需要明确其几个关键的属性定义:
状态字段 (state)
state 是同步器的核心状态标识,通常用来表示资源是否可用。例如,在可重入锁中,state 代表锁的重入次数;在 CountDownLatch 中,state 代表剩余计数值。修改该字段需要使用 volatile 保证可见性,并通过 CAS 操作保证原子性。
// 同步器的核心状态
private volatile int state;
同步队列头尾指针
当多个线程竞争锁失败时,会被封装成 Node 节点放入同步队列中排队阻塞。AQS 通过两个 volatile 指针管理这个双向链表:
// 同步队列的头节点
private transient volatile Node head;
// 同步队列的尾节点
private transient volatile Node tail;
节点结构 (Node)
Node 是同步队列中的基本元素,它缓存了当前线程、节点状态以及前后指针关系。其中最值得关注的是 waitStatus 字段,它控制着节点的行为:
static final class Node {
// 节点等待状态
// SIGNAL (-1): 表示后继节点需要被唤醒
// CANCELLED (1): 节点已取消
// CONDITION (-2): 节点处于条件队列
// PROPAGATE (-3): 共享模式下下一次 acquires 应当无条件传播
volatile int waitStatus;
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前线程
}
条件队列 (ConditionObject)
条件队列同样是链表结构,但与同步队列不直接交互。它主要用于实现类似 Object.wait()/notify() 的高级线程协作功能。通过调用 new ConditionObject() 可创建一个条件对象。一个 Lock 可以绑定多个不同的 Condition,从而实现更精细的线程通知策略(如生产者-消费者模型中的“满”和“空”条件)。
2、同步器状态辨析
在阅读 AQS 源码时,务必区分两种状态:
- state: 全局状态,由 AQS 子类(如锁的实现)使用,用于判断资源占用情况。
- waitStatus: 节点状态,由 AQS 内部管理,用于控制线程的阻塞、唤醒或取消行为。
3、获取锁机制详解
获取锁的过程主要由 acquire 系列方法驱动。无论排他锁还是共享锁,其核心思想都是:先尝试直接获取,失败则进入队列排队等待。
3.1、独占模式获取锁 (acquire)
以 ReentrantLock 为例,调用 lock.lock() 最终会触发 AQS 的 acquire(arg) 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法包含三个清晰的步骤:
- 尝试获取: 调用
tryAcquire(arg)。这是留给子类实现的模板方法,通过 CAS 修改state。如果成功,直接返回。 - 包装入队: 如果获取失败,调用
addWaiter(Node.EXCLUSIVE)将当前线程包装为独占模式的 Node,并尝试追加到同步队列尾部。 - 排队阻塞: 调用
acquireQueued。这是一个自旋过程,节点会在队列中反复尝试获取锁,并在适当时候挂起(park),直到拿到锁或被中断。
核心细节:addWaiter 与 enq
为了优化性能,addWaiter 采用了一次非阻塞的 CAS 尝试,如果成功则快速返回;只有失败时才进入 enq 方法进行无限循环重试,确保节点最终能加入队尾。
核心细节:acquireQueued 与 挂起逻辑
acquireQueued 是整个 AQS 最复杂的环节之一,它保证了线程安全地阻塞和唤醒:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 如果前驱是头节点,说明有机会获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 把自己设为头节点
p.next = null; // 回收旧头节点
failed = false;
return interrupted;
}
// 检查是否需要挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
其中 shouldParkAfterFailedAcquire 负责处理前驱节点的 waitStatus:
- 如果前驱状态为
SIGNAL,说明可以放心挂起当前节点。 - 如果前驱状态为
CANCELLED(>0),则向前查找有效节点并将当前节点链接过去。 - 否则,通过 CAS 将前驱状态改为
SIGNAL,返回 false 促使外层循环再次尝试(因为此时还不能挂起)。
3.2、共享模式获取锁 (acquireShared)
共享锁(如 ReadWriteLock 的读锁)允许同时存在多个持有者。其入口为 acquireShared(arg),逻辑框架与独占锁类似,但在获得锁后的处理上有显著差异。
在独占模式中,线程拿到锁后仅将自己设为 Head 并切断与旧 Head 的联系。而在共享模式中,如果当前队列中还有后续节点,且允许继续共享(propagate > 0),则需要唤醒后续等待的节点一起竞争锁或自动获得许可。
setHeadAndPropagate 方法不仅执行设置 Head 的操作,还会调用 doReleaseShared 去检查并唤醒后继节点:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); // 唤醒后继
}
// ... 处理传播状态 ...
}
if (h == head) break;
}
}
总结
AQS 的设计极其精巧,通过 state 统一资源状态,通过 CLH 变体队列统一管理竞争失败的线程,通过模板方法将锁的具体业务逻辑剥离给子类。掌握 acquire 的流程及其背后的自旋、CAS 和 Park/Unpark 机制,是理解 Java 并发锁体系的前提。
以上就是java同步器AQS架构AbstractQueuedSynchronizer原理解析的详细内容,更多关于java同步器AbstractQueuedSynchronizer的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









