Java AQS 详解

1,224 阅读12分钟

本文源码均来源于 JDK 1.8

什么是 AQS

AQS 指的是 AbstractQueuedSynchronizer 这个类, 它提供了一个用于构建依赖于 FIFO 等待队列的阻塞锁和与之相关的同步器的框架, 比如 ReentrantLockCountDownLatch 都是基于 AQS 的

来看一看 AbstractQueuedSynchronizer 的源码里大概都有些什么,这里只列举部分核心方法,其它核心方法会在后面详细介绍

public abstract class AbstractQueuedSynchronizer 
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    public class ConditionObject implements Condition, java.io.Serializable {}
    
    static final class Node {}
    
    // 同步状态,使用volatile修饰保证线程可见性
    private volatile int state;
    
    protected final int getState() {}
    protected final void setState(int newState) {}
    // 原子地(CAS)设置同步状态值
    protected final boolean compareAndSetState(int expect, int update) {}
    
    // 独占模式获取同步状态,不理解什么是独占模式也不要紧,后面会详细解释
    public final void acquire(int arg) {}
    // 独占模式释放同步状态
    public final boolean release(int arg) {}
    // 尝试使用独占模式获取同步状态
    protected boolean tryAcquire(int arg) {}
    // 尝试使用独占模式释放同步状态
    protected boolean tryRelease(int arg) {}
}

可以看到,AbstractQueuedSynchronizer 拥有 2 个内部类:ConditionObjectNode。如果使用过 ReentrantLock,那么对于 lock.newCondition() 一定非常熟悉,newCondition 返回的就是 ConditionObject 实例, 但本文暂不对 ConditionObject 做深入探讨

AQS 原理

AQS 核心思想:

  1. 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
  2. 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中

CLH 队列

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系,即表现为双向链表)。 AQS 将每个请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node,也即前面看到的 Node 内部类)来实现锁的分配

下图是 CLH 队列示意图,注意 CLH 队列包含 head 节点,但阻塞队列不包含 head 节点,因为 head 节点是占有锁的线程所在节点,这一点在后面的源码分析也能体现

来看一下 Node 类的源码

static final class Node {
    /** 默认为共享模式的节点 */
    static final Node SHARED = new Node();
    /** 独占模式 */
    static final Node EXCLUSIVE = null;
    
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    // 本文暂不分析此属性,此属性用于Condition
    Node nextWaiter;

    /** waitStatus value:线程已取消,不再争抢这个锁 */
    static final int CANCELLED =  1;
    /** waitStatus value:当前node的后继节点对应的线程需要被唤醒 */
    static final int SIGNAL    = -1;
    /** waitStatus value:线程正在等待,表明还在队列中,用于 Condition */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;
}

可以发现,在 CLH 队列中,每个 Node 保存了线程的引用、状态、前驱节点、后继节点,并且状态有 4 个取值分别对应不同的状态,如果大于 0,则代表这个线程取消了等待

同步状态的获取 - 抢锁

独占式

独占式,指同一时刻只能有一个线程持有同步状态(锁)

acquire(int arg)

此方法对中断不敏感,也就是说如果线程获取同步状态失败加入到 CLH 同步队列,后续对该线程进行中断操作时,线程并不会从同步队列中移除

public final void acquire(int arg) {
    // 首先调用tryAcquire,试一下,如果成功就不需要进队列排队了
    // 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就理所当然可以直接获取
    // 对于公平锁的语义:本来就没人持有锁,根本没必要进队列等待
    if (!tryAcquire(arg) &&
        // tryAcquire(arg) 没有成功,这个时候需要把当前线程挂起,放到阻塞队列中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 将当前线程加入到 CLH 同步队列尾部
// 参数 mode 此时是 Node.EXCLUSIVE,代表独占模式
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // tail != null => CLH 队列不为空
    if (pred != null) {
        node.prev = pred;
        // 如果成功,tail == node
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    
    // 如果执行到这里,说明 pred == null(队列是空的) 或者 CAS失败(有线程在竞争入队)
    enq(node);
    return node;
}

// 采用自旋的方式入队
// 自旋的语义:CAS 设置 tail 过程中,竞争一次竞争不到,就多次竞争,总会排到
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 细心的同学会发现,原来 head 和 tail 在 AbstractQueuedSynchronizer 初始化的时候都是 null
        if (t == null) { // Must initialize
            // CAS 初始化 head,可能有很多线程同时进来
            // 如果对 Node 的源码还有印象,可以知道现在 head 没有线程引用、前驱节点和后继节点
            // 注意:这个时候 head 节点的 waitStatus == 0,Node() 构造方法为空实现
            if (compareAndSetHead(new Node()))
                // 注意这里没有 return,只是初始化了 head 节点,并将 tail 指向 head
                tail = head;
        } else {
            // 如果 head 节点在上一轮循环才初始化,那么 node.prev = head
            node.prev = t;
            // 将当前线程排到队尾,有线程竞争的话排不上重复排
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

// 参数 node,经过 addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
// 如果此方法返回 true 的话,意味着 acquire 方法将进入 selfInterrupt 方法
// static void selfInterrupt() {
//     Thread.currentThread().interrupt();
// }
// 所以正常情况下应该返回 false
// 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // predecessor() 返回 node 的上一个节点
            final Node p = node.predecessor();
            // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个
            // 注意,阻塞队列不包含 head 节点,head 一般指的是占有锁的线程,head 后面的才称为阻塞队列
            // 所以当前节点可以去试抢一下锁,这里说一下为什么可以去试试:
            //   1.它是队头
            //   2.当前的 head 有可能是刚刚初始化的 node
            //     enq(node) 方法里面有提到,head 是延时初始化的,而且 new Node() 的时候没有设置任何线程
            //     也就是说,当前的 head 不属于任何一个线程,所以作为队头,可以去试一试
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 执行到这里,说明上面的 if 分支没有成功,没有抢到锁
            // 要么当前 node 不是队头,要么就是 tryAcquire(arg) 没有抢赢别人
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // tryAcquire() 方法抛异常的情况下,failed == true
        // 取消抢锁操作
        if (failed)
            cancelAcquire(node);
    }
}

// 没有抢到锁时才会执行此方法,此方法的意思是:当前线程没有抢到锁,是否需要挂起当前线程
// 第一个参数是前驱节点,第二个参数是当前线程的节点
private static boolean shouldParkAfterFailedAcquire(AbstractQueuedSynchronizer.Node pred, AbstractQueuedSynchronizer.Node node) {
    int ws = pred.waitStatus;
    // 前驱节点的 waitStatus == -1,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
    if (ws == AbstractQueuedSynchronizer.Node.SIGNAL)
        return true;
    // 前驱节点 waitStatus大于 0 ,之前说过,大于 0 说明前驱节点取消了排队
    // 这里需要知道:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的
    // 所以下面这块代码说的是将当前节点的 prev 指向 waitStatus<=0 的节点
    // 因为当前线程得依赖前驱节点来唤醒呢
    // 到这里可以发现 Node 的 waitStatus 属性其实是对于后继节点有效的
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 执行到这里表示 waitStatus 不等于 -1 和 1,那也就是只可能是 0、-2、-3
        // 在我们前面的源码中,都没有看到有设置 waitStatus 的,所以每个新的 node 入队时,waitStatu 都是0
        // 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
        // 用 CAS 将前驱节点的 waitStatus 设为 Node.SIGNAL(也就是-1)
        compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizer.Node.SIGNAL);
    }
    // 这里返回 false,在 acquireQueued 方法中会进行下一轮 for 循环
    // 直到将前驱节点的 waitStatus 置为 -1,再下一轮就会通过第一个 if 分支返回 true
    return false;
}

// 如果 shouldParkAfterFailedAcquire(p, node) 返回 true,
// 那么需要执行 parkAndCheckInterrupt()
// 因为前面返回 true,所以需要挂起线程,这个方法就是负责挂起线程的
// 这里用了 LockSupport.park(this) 来挂起线程,然后就停在这里了,等待被唤醒
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
acquireInterruptibly(int arg)

此方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常 InterruptedException

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireInterruptibly(int arg)acquire(int arg) 仅有两个差别:

  1. 方法声明抛出 InterruptedException 异常
  2. 在中断方法处不再是使用 interrupted 标志,而是直接抛出 InterruptedException 异常
tryAcquireNanos(int arg, long nanosTimeout)

此方法为 acquireInterruptibly 方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回 false,否则返回 true

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

针对超时控制,程序首先记录唤醒时间 deadlinedeadline = System.nanoTime() + nanosTimeout。如果获取同步状态失败,则需要计算出需要休眠的时间间隔 nanosTimeout(= deadline - System.nanoTime()),如果 nanosTimeout <= 0 表示已经超时了,返回 false,如果大于 spinForTimeoutThreshold(1000L) 则需要休眠 nanosTimeout ,如果 nanosTimeout <= spinForTimeoutThreshold ,就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让 nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS 会进行无条件的快速自旋

共享式

共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞

acquireShared(int arg)

此方法对中断不敏感,也就是说如果线程获取同步状态失败加入到 CLH 同步队列,后续对该线程进行中断操作时,线程并不会从同步队列中移除

public final void acquireShared(int arg) {
    // tryAcquireShared(arg) < 0 代表失败
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                // r >= 0 表示获取到同步状态
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

AQS 也提供了共享式响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)tryAcquireSharedNanos(int arg, long nanosTimeout),这里就不做解释了

同步状态的释放 - 解锁

独占式

独占式同步状态释放使用 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;
}

// 参数 node 是 head 头结点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus == 1)
    // 从队尾往前找,找到 waitStatus<=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);
}

共享式

共享式同步状态释放使用 releaseShared(int arg) 方法

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // Node.SIGNAL == -1
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        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);
}

总结

在并发环境下,加锁和解锁需要以下三个部件的协调:

  1. 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state 进行 +1 就可以,解锁就是 -1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对。然后唤醒等待队列中的第一个线程,让其来占有锁
  2. 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 LockSupport.unpark(thread) 来唤醒线程
  3. 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用


参考:

~~~~~~~~【死磕Java并发】—–J.U.C之AQS(一篇就够了)

~~~~~~~~一行一行源码分析清楚AbstractQueuedSynchronizer