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

一、核心属性

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

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

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

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

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

二、重要内部类:Node

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

Node 的核心属性

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

节点模式

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

三、核心方法源码解析

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

java 复制代码
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):将线程加入等待队列

java 复制代码
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): 自旋入队

java 复制代码
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):队列中线程的自旋与阻塞

java 复制代码
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):释放独占资源

java 复制代码
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(): 判断是否应该进入阻塞

java 复制代码
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?因为设置状态后,当前线程还不能立即阻塞,需要再次尝试获取锁,以防在设置状态期间锁已经被释放。如果此时获取成功,就无需阻塞了。

相关推荐
yhyyht2 小时前
InfluxDB入门记录(三)flux-dsl
数据库·后端
Re_zero2 小时前
以为用了 try-with-resources 就稳了?这三个底层漏洞让TCP双向通讯直接卡死
java·后端
SimonKing2 小时前
Fiddler抓包完全指南:从安装配置到抓包,一文讲透
java·后端·程序员
IT_陈寒2 小时前
Python 性能提升50%的5个魔法技巧,90%的人还不知道!
前端·人工智能·后端
CodeSheep3 小时前
“渐渐能理解为何不愿意雇佣35岁以上程序猿。去年换了份工作,组里4位组员其中3位40+,发现其实最大的问题并不是说精力不济卷不动”
前端·后端·程序员
摸鱼的春哥3 小时前
【实战】吃透龙虾🦞,你写的Agent也能支持Skills渐进式披露
前端·javascript·后端
明月_清风3 小时前
从“能用”到“专业”:构建生产级装饰器与三层逻辑拆解
后端·python
万少10 小时前
小龙虾(openclaw),轻松玩转自动发帖
前端·人工智能·后端
Jagger_12 小时前
AI 洪水淹到脖子了:剩下的是什么?我们该往哪儿跑?
后端