AbstractQueuedSynchronizer(AQS)核心源码解析:属性、方法与内部类深度剖析

2 阅读6分钟

一、核心属性

AQS 的内部状态主要依靠以下几个关键字段维护:

属性类型说明
statevolatile int同步状态。这是 AQS 最核心的字段,代表共享资源的数量。在 ReentrantLock 中表示锁被持有的次数(可重入),在 Semaphore 中表示剩余许可数。对 state 的操作必须保证线程安全。
headvolatile Node等待队列的头结点。头结点是一个“哨兵节点”,它本身不持有等待的线程,其 next 节点(head.next)才是队列中第一个真正等待的线程节点。
tailvolatile Node等待队列的尾结点。每次有新线程加入队列时,都会通过 CAS 方式更新 tail 指向新的尾节点。
exclusiveOwnerThreadThread(继承自 AbstractOwnableSynchronizer持有独占锁的线程。在独占模式下,用来记录当前占有同步状态的线程,用于实现可重入等特性。

关于 state 的三个操作模板方法

AQS 提供了三个 protected final 的方法来操作 state,子类可以通过它们来修改同步状态:

  • int getState():获取当前同步状态。
  • void setState(int newState):设置当前同步状态。
  • boolean compareAndSetState(int expect, int update):使用 CAS 原子性地设置同步状态,保证并发修改的安全性。

二、重要内部类:Node

Node 是构成 AQS 等待队列的基本单位,它是一个双向链表的节点。使用双向链表可以方便地删除中间节点(如取消的节点),以及从后向前遍历。

Node 的核心属性

属性类型说明
prevvolatile Node前驱节点。
nextvolatile Node后继节点。
threadvolatile Thread当前节点所持有的线程。
waitStatusvolatile int节点的等待状态,共有以下 5 种:
CANCELLED (1)表示线程已取消(因超时或中断)。处于此状态的节点不会再被唤醒。
SIGNAL (-1)表示当前节点的后继节点需要被唤醒(即当前节点释放锁或取消时,必须唤醒后继节点)。
CONDITION (-2)表示当前节点在条件队列(Condition)中等待。
PROPAGATE (-3)仅在共享模式下使用,表示下一次共享式获取应该无条件传播下去。
`0初始状态,表示当前节点在同步队列中,等待获取锁。
nextWaiterNode在条件队列中指向下一个等待节点;在同步队列中,也可用于标记模式(独占或共享)。

节点模式

  • static final Node SHARED = new Node():标记节点为共享模式
  • static final Node EXCLUSIVE = null:标记节点为独占模式

三、核心方法源码解析

1. acquire(int arg):独占获取的入口

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

解析:

  • 首先尝试 tryAcquire,成功则直接返回。
  • 失败则通过 addWaiter 入队,然后 acquireQueued 使线程在队列中等待。
  • 如果 acquireQueued 返回 true(表示等待过程中被中断过),则调用 selfInterrupt() 在获取成功后补上中断标志。

2. addWaiter(Node mode):将线程加入等待队列

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 快速入队:直接 CAS 设置尾节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速入队失败,则通过 enq 方法自旋入队
    enq(node);
    return node;
}

解析:

  • 首先创建一个持有当前线程的节点,模式由参数 mode 指定(独占或共享)。
  • 快速路径: 如果队列已存在(tail != null),尝试直接 CAS 更新尾节点。若成功,则双向链接完成,直接返回节点。这是最常见的无竞争情况,效率很高。
  • 完整路径: 如果队列为空或 CAS 失败(说明有其他线程也在同时入队并修改了 tail),则调用 enq(node) 自旋入队。

3. enq(Node mode): 自旋入队

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,需要初始化
            if (compareAndSetHead(new Node())) // 设置头节点(哨兵节点)
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq 通过自旋不断尝试,直到入队成功。它处理了两种场景:

如果队列为空(tail == null),先初始化一个头节点(哨兵节点),然后再次循环。

如果队列非空,则执行与快速入队相同的 CAS 设置尾节点的操作,直到成功。

4. acquireQueued(Node node, int arg):队列中线程的自旋与阻塞

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 获取前驱节点
            if (p == head && tryAcquire(arg)) { // 如果是第一个等待节点,尝试获取
                setHead(node);                  // 获取成功,设为新头节点
                p.next = null;                   // 断开原头节点,帮助 GC
                failed = false;
                return interrupted;
            }
            // 如果不是头节点或获取失败,判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;               // 记录中断,但不退出
        }
    } finally {
        if (failed)
            cancelAcquire(node);                  // 如果发生异常,取消获取
    }
}

解析:

当前线程进入队列后,在 for(;;) 中自旋。

**检查前驱:**如果前驱是头节点(即当前节点是队列中第一个等待线程),则再次调用 tryAcquire 尝试获取锁。这是 FIFO 公平性的体现。

获取成功: 将自己设为新的头节点,断开原头节点的引用,返回中断标志。

获取失败: 调用 shouldParkAfterFailedAcquire 检查并设置前驱节点的状态。如果前驱状态为 SIGNAL,则当前线程可以安全地阻塞;否则会跳过取消的节点或尝试将前驱设为 SIGNAL 后重试。

阻塞: 当 shouldParkAfterFailedAcquire 返回 true 时,调用 parkAndCheckInterrupt() 通过 LockSupport.park(this) 阻塞当前线程。唤醒后检查是否被中断,并记录中断标志。

中断处理: acquireQueued 不响应中断,只记录中断标志,最终返回给上层,由上层(如 acquire)决定是否补上中断。

5. release(int arg):释放独占资源

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);   // 唤醒后继节点
        return true;
    }
    return false;
}

解析:

调用 tryRelease 尝试释放资源(由子类实现,例如 ReentrantLock 中减少 state)。

如果释放成功,检查头节点是否有效(waitStatus != 0 表示有后继需要唤醒),然后调用 unparkSuccessor 唤醒后继线程。

unparkSuccessor 会找到头节点的下一个未取消节点,并使用 LockSupport.unpark(thread) 唤醒它。

6、shouldParkAfterFailedAcquire(): 判断是否应该进入阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;          // 获取前驱节点的等待状态
    if (ws == Node.SIGNAL)             // 情况1:前驱已设置唤醒信号
        return true;                    // 当前线程可以安全地阻塞
    if (ws > 0) {                        // 情况2:前驱已取消(CANCELLED)
        do {
            node.prev = pred = pred.prev; // 跳过所有连续取消的前驱
        } while (pred.waitStatus > 0);
        pred.next = node;                 // 重新建立链接
    } else {                               // 情况3:前驱状态为 0 或 PROPAGATE
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 尝试将前驱设为 SIGNAL
    }
    return false;                          // 返回 false,表示不能阻塞,需要重试
} 

1. 前驱状态为 SIGNAL(-1) :前驱节点已经承诺:当它释放锁或被取消时,会负责唤醒当前节点,返回 true。

2. 前驱状态为 CANCELLED(>0):该节点已因超时或中断而被取消。取消的节点不应再作为唤醒的责任节点,因此需要跳过所有连续取消的前驱,直到找到一个未取消的节点。

3. 前驱状态为 0 或 PROPAGATE(-3) : 如果前驱状态为初始状态 0(表示节点刚入队,还未设置任何信号),或者是共享模式下的 PROPAGATE,则需要通过 CAS 将前驱状态设置为 SIGNAL,以表明当前节点需要被唤醒。

设置成功后,返回 false。为什么返回 false?因为设置状态后,当前线程还不能立即阻塞,需要再次尝试获取锁,以防在设置状态期间锁已经被释放。如果此时获取成功,就无需阻塞了。