Bootstrap

AQS源码级别解析

AQS

AQS 全称是 AbstractQueuedSynchronizer。顾名思义就是一个抽象的(可被继承复用),内部存在排队(竞争资源的线程排队)的同步器(对共享资源和线程进行同步管理)。


属性

首先来看 AQS 的成员属性。

private volatile int state;

state 是用于判断共享资源是否正在被占用的标记位,volatile 保证了线程之间的可见性(可见性就是当一个线程修改了 state 的值,其他线程下一次读取都能读到最新值)。

为什么 state 的类型是 int?而不是 boolean?

线程获取锁有两种模式,独占和共享。当一个线程以独占模式获取锁时,其他线程任何必须等待,而当一个线程以共享模式获取锁时,其他也想以共享模式获取锁的线程也能够一起访问共享资源,但其他想以独占模式获取锁的线程需要等待。所以,共享模式下,可能有多个线程正在共享资源,所以 state 需要表示线程占用数量,因此是 int 值。

private transient volatile Node head;
private transient volatile Node tail;

AQS 中存在一个队列用于对等待线程进行管理,这个队列通过一个 FIFO 的双向链表来实现,head 和 tail 变量表示这个队列的头尾。


内部类

队列中每个节点的类型是内部类 Node。

static final class Node {
    // 共享模式
    static final Node SHARED = new Node();
    // 独占模式
    static final Node EXCLUSIVE = null;

    // 节点已被取消(线程超时或中断)
    static final int CANCELLED =  1;

    // 当前节点的后继节点需要被唤醒(unpark)
    static final int SIGNAL    = -1;

    // 当前节点在条件队列中等待
    static final int CONDITION = -2;

    // 共享模式下,释放操作需要传播到后续节点
    static final int PROPAGATE = -3;
    
    // 线程的等待状态
    volatile int waitStatus;

    // 前驱节点
    volatile Node prev;

    // 后继节点
    volatile Node next;

    // 节点对应的线程
    volatile Thread thread;

    // 指向下一个等待节点
    Node nextWaiter;

    // 判断节点是否在共享模式下等待
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 获取前驱节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

队列里的节点有两种模式,独占和共享。

这里重点关注的是 waitStatus 这个属性,它是一个枚举值,AQS 工作时必然伴随着 Node 的 waitStatus 值的变化,如果理解了 waitStatus 的变化时机,对理解 AQS 整个工作原理有很大帮助。

waitStatus 主要包含四个状态:

  • 0,节点初始化默认值活节点已经释放锁
  • CANCELLED 为 1,表示当前节点获取锁的请求已经被取消了
  • SIGNAL 为 -1,表示当前节点的后续节点需要被唤醒
  • CONDITION 为 -2,表示当前节点正在等待某一个 Condition 对象,和条件模式相关
  • PROPAGATE 为 -3,传递共享模式下锁释放状态,和共享模式相关

方法

有两种使用场景:

  1. 尝试获取锁,不管有没有获取到,立即返回。
  2. 必须获取锁,如果当前时刻锁被占用,则进行等待。
// try acquire
protected boolean tryAcquire(int arg){
    throw new UnsupportedOperationException();
}

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

tryAcquire 是一个被 protected 修饰的方法,参数是一个 int 值,代表对 int state 的增加操作,返回值是 boolean,代表是否成功获得锁。

该方法只有一行实现 throw new UnsupportedOperationException(),意图很明显,AQS 规定继承类必须 override tryAcquire 方法,否则直接抛出 UnsupportedOperationException()。为什么这里一定要上层自己实现呢?因为尝试获取锁这个操作中可能包含某些业务自定义的逻辑,比如是否“可重入”等。

若上层调用 tryAcquire 返回 true,线程获得锁,此时可以对响应的共享资源进行操作,使用完之后再进行释放。如果调用 tryAcquire 返回 false,且上层逻辑上不想等待锁,那么可以自己进行相应的处理;如果上层逻辑选择等待锁,那么可以直接调用 acquire 方法,acquire 方法内部封装了复杂的排队处理逻辑,非常易用。

acquire 被 final 修饰,表示不允许子类擅自 override。

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

if 条件包含了两部分:

  • !tryAcquire(arg)
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

这里的意思是,如果 tryAcquire 获取锁成功,那么 !tryAcquire 为 false,说明已经获取锁,根本不用参与排队,也就不用再执行后续判断条件。根据判断条件的短路规则,直接返回。

如果 tryAcquire 返回 false,说明需要排队,那么就进而执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)acquireQueued 方法其中嵌套了 addWaiter 方法。

private Node addWaiter(Node mode) {
    // 创建一个新节点,mode 可以是 Node.EXCLUSIVE(独占模式)或 Node.SHARED(共享模式)
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速将节点添加到队列尾部
    Node pred = tail;
    if (pred != null) {
        node.prev = pred; // 设置新节点的前驱为当前尾节点
        if (compareAndSetTail(pred, node)) { // CAS 设置新节点为尾节点
            pred.next = node; // 将当前尾节点的后继指向新节点
            return node;
        }
    }
    // 如果快速添加失败,调用 enq 方法进行完整入队操作
    enq(node);
    return node;
}

顾名思义,这个方法的作用就是将当前线程封装成一个 Node,然后加入等待队列,返回值即为该 Node。这段主要逻辑就是,首先创建一个 Node 对象,我们需要将其插入队尾。但是我们需要考虑多线程场景,即假设存在多个线程正在调用 addWaiter 方法。

新建 pred 节点引用,指向当前的尾节点,如果尾节点不为空,那么下面将进行三步操作:

  1. 将当前节点的 pre 指针指向 pred 节点(尾节点)。
  2. 尝试通过 CAS 操作将当前节点置为尾节点。
    • 如果返回 false,说明 pred 节点已经不是尾节点,在上面的执行过程中,尾节点已经被其他线程修改,那么退出判断,调用 enq 方法,准备重新进入队列。
    • 如果返回 true,说明 CAS 操作之前,pred 节点依旧是尾节点,CAS 操作使当前 node 顺利成为尾节点。若当前 node 顺利成为尾节点,那么 pred 节点和当前 node 之间的相对位置已经确定,此时将 pred 节点的 next 指针指向当前 node,是不会存在线程安全问题的。

由于在多线程环境下执行,这里存在三个细节,也是该方法中的重点。

某线程执行到第 8 行时,pred 引用指向的对象可能已经不再是尾节点,所以 CAS 失败;

如果 CAS 成功,诚然 CAS 操作是具有原子性的,但是 9、10 两行在执行时并不具备原子性,只不过此时 pred 节点和当前 node 之间的相对位置已经确定,其他线程只是正在插入新的尾节点,并不会影响到这里的操作,所以是线程安全的。

需要记住的是,当前后两个节点建立连接的时候,首先是后节点的 pre 指向前节点,当后节点成功成为尾节点后,前节点的 next 才会指向后节点。

如果理解了这些,我们再来看第 14 行。当程序运行到这一行,说明出现了两种情况之一:

  • 队列为空
  • 快速插入失败,想要进行完整流程的插入,这里所说的快速插入,指的就是 6-12 行的逻辑,当并发线程较少的情况下,快速插入成功率很高,程序不用进入完整流程插入,效率会更高。

既然程序来到了第 14 行,那么我们就来看看完整流程的插入是什么样子的。

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)) { // CAS 设置新节点为尾节点
                t.next = node; // 将当前尾节点的后继指向新节点
                return t;
            }
        }
    }
}

这个方法的逻辑就是在最外层加上了一层死循环,如果队列未初始化(tail == null),那么就尝试初始化,如果尾节点插入失败,那么就不断重试,直到插入成功为止。

一旦 addWaiter 成功之后,AQS 在各个线程中维护了当前 NodewaitStatus,根据不同的状态,程序来做出不同的操作。通过调用 acquireQueue 方法,开始对 NodewaitStatus 进行跟踪维护。

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

继续看 acquireQueue 源码。

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; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node); // 如果失败,取消节点
    }
}

首先,acquireQueued 方法内定义了一个局部变量 failed,初始值为 true,意思是默认失败。还有一个变量 interrupted,初始值为 false,意思是等待锁的过程中当前线程没有被中断。再来看看在整个方法中,哪里用到了这两个变量?

  1. 第 11 行,return 之前,failed 值会改为 false,代表执行成功,并且返回 interrupted 值。
  2. 第 15 行,如果满足判断条件,interrupted 将会被改为 true,最终在第 11 行被返回出去。
  3. 第 18 行,finally 块中,通过判断 failed 值来进行一个名为 cancelAcquire 的操作,即取消当前线程获取锁的行为。

那么我们基本可以将 acquireQueued 分为三部分。

  • 7-11 行:当前置节点为 head,说明当前节点有权限去尝试拿锁,这是一种约定。如果 tryAcquire(arg) 返回 true,代表拿到了锁,那么顺理成章,函数返回。如果不满足第 7 行的条件,那么进入下一阶段。
  • 13-15 行:if 中包含两个方法,看名字(详细方法体后续再看)是首先判断当前线程是否需要挂起等待?如果需要,那么就挂起,并且判断外部是否调用线程中断;如果不需要,那么继续尝试拿锁。
  • 18-19 行:如果 try 块中抛出非预期异常,那么取消当前线程获取锁的行为。

这里有三点需要着重关注一下。

  1. 一个约定:head 节点代表当前正在持有锁的节点。若当前节点的前置节点是 head,那么该节点就开始自旋地获取锁。一旦 head 节点释放,当前节点就能第一时间获取到。
  2. shouldParkAfterFailedAcquireparkAndCheckInterrupt 方法体细节。
  3. interrupted 变量最终被返回出去后,上层 acquire 方法判断该值,来选择是否调用当前线程中断。这里属于一种延迟中断机制。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱节点状态为 SIGNAL,表示它会通知后续节点,当前线程可以安全挂起
        return true;
    if (ws > 0) {
        // 前驱节点状态为 CANCELLED(>0),需跳过这些节点,直到找到未取消的前驱
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前驱节点状态为 0 或 PROPAGATE,需将其状态设置为 SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

若当前节点没有拿锁的权限或拿锁失败,那么将会进入 shouldParkAfterFailedAcquire 判断是否需要挂起(park),方法的参数是 pred Node 和当前 Node 的引用。

  • 首先获取 pred NodewaitStatus,若 predwaitStatusSIGNAL,说明前置节点也在等待拿锁,并且之后会唤醒当前节点,所以当前线程可以挂起休息,返回 true
  • 如果 ws > 0,说明 predwaitStatusCANCEL,所以可以将其从队列中删除。这里通过从后向前搜索,将 pred 指向搜索过程中第一个 waitStatus 为非 CANCEL 的节点。相当于链式地删除被 CANCEL 的节点。然后返回 false,代表当前节点不需要挂起,因为 pred 指向了新的 Node,需要重试外层的逻辑。
  • 除此之外,pred 的 ws 还有两种可能,0PROPAGATE,有人可能会问,为什么不可能是 CONDITION? 因为 waitStatus 只有在其他条件模式下,才会被修改为 CONDITION,这里不会出现,并且只有在共享模式下,才可能出现 waitstatusPROPAGATE,暂时也不用管。那么在独占模式下 ws 在这里只会出现 0 的情况。0 代表 pred 处于初始化默认状态,所以通过 CAS 将当前 predwaitStatus 修改为 SIGNAL,然后返回 false,重试外层逻辑。

这个方法开始涉及到对 Node 的 waitSatus 的修改,相对比较关键。

如果 shouldParkAfterFailedAcquire 返回 false,那么再进行一轮重试;如果返回 true,代表当前节点需要被挂起,则执行 parkAndCheckInterrupt 方法。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 挂起当前线程
    return Thread.interrupted(); // 返回线程的中断状态,并清除中断标志
}

这个方法只有两行,对当前线程进行挂起的操作。这里 LockSupport.park(this) 本质是通过 UNSAFE 下的 native 方法调用操作系统原语来将当前线程挂起。
此时当前 Node 中的线程将阻塞在此处,直到持有锁的线程调用 release 方法,release 方法会唤醒后续节点。

那这边的 return Thread.interrupted() 又是什么意思呢? 这是因为在线程挂起期间,该线程可能会被调用中断方法,线程在 park 期间,无法响应中断,所以只有当线程被唤醒,执行到第 3 行才会去检查 park 期间是否被调用过中断,如果有的话,则将该值传递出去,通过外层来响应中断。
通过对 acquireQueued 这个方法的分析,我们可以这么说,如果当前线程所在的节点处于头节点的后一个,那么它将会不断去尝试拿锁,直到获取成功。否则进行判断,是否需要挂起。这样就能保证 head 之后的一个节点在自旋 CAS 获取锁,其他线程都已经被挂起或正在被挂起。这样就能最大限度地避免无用的自旋消耗 CPU。

既然大量线程被挂起,那么就会有被唤醒的时机。上面也提到,当持有锁的线程释放了锁,那么将会尝试唤醒后续节点。下面看一下 release 方法。

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 尝试释放同步状态
        Node h = head; // 获取头节点
        if (h != null && h.waitStatus != 0) // 如果头节点存在且状态不为0
            unparkSuccessor(h); // 唤醒后继节点
        return true;
    }
    return false;
}
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire 一样,tryRelease 也是 AQS 开放给上层自由实现的抽象方法。
release 中,假如尝试释放锁成功,下一步就要唤醒等待队列里的其他节点,这里主要来看 unparkSuccessor 这个方法。参数是 head Node。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus; // 获取当前节点的等待状态
    if (ws < 0) // 如果状态为 SIGNAL 或 PROPAGATE
        compareAndSetWaitStatus(node, ws, 0); // 清除状态

    Node s = node.next; // 获取当前节点的后继节点
    if (s == null || s.waitStatus > 0) { // 如果后继节点为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从尾节点向前遍历
            if (t.waitStatus <= 0) // 找到第一个未取消的节点
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 唤醒后继节点的线程
}

获取 head 的 waitStatus,如果不为 0,那么将其置为 0,表示锁已释放。接下来获取后续节点如果后续节点为 null 或者处于 CANCELLED 状态,那么从后往前搜索,找到除了 head 外最靠前且非 CANCELLED 状态的 Node,对其进行唤醒,让它起来尝试拿锁。
这时,拿锁、挂起、释放、唤醒都能够有条不紊,且高效地进行。
关于 9-11 行,可能有人疑惑,为什么不直接从头开始搜索,而是要花这么大力气从后往前搜索?

其实是和 addWaiter 方法中,前后两个节点建立连接的顺序有关。我们看:

  1. 后节点的 pre 指向前节点
  2. 前节点的 next 才会指向后节点

这两步操作在多线程环境下并不是原子的,也就是说,如果唤醒是从前往后搜索,那么可能前节点的 next 还未建立好,那么搜索可能会中断。

;