JUC AQS(抽象队列同步器)

882 阅读11分钟

AQSJUC 的基石。

很多 JUC 相关的工具类其内部就用到了 AQS,而这些工具类已经基本足够覆盖大部分的业务场景了,这就使得我们即便不了解 AQS,也能利用这些工具类顺利进行开发。

我们学习 AQS 的目的主要是想理解其背后的原理、学习设计思想,以提高技术应对面试

1、什么是 AQS

AQS 的重要性

可以先看看 AQS 应用在了哪些地方,虽然我们从未直接的去使用过它:

image-20220216130232568

如图所示,AQSReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理。

我觉得这就是封装思想的体现。比如 HashMap,内部不就封装了一个 Node 节点来存储键值对,Lock 接口我们用的时候就 lock()、unlock() 方法加锁解锁,其实它的底层调用 Sync 的相关方法,而 Sync 又继承 AQS。我觉得封装很重要的一个特点,内部的细节对我们是不可见的。

锁和协作类有共同点

阀门功能

ReentrantLockSemaphore,二者之间有没有什么共同点?

其实它们都可以当做一个阀门来使用。比如我们把 Semaphore 的许可证数量设置为 1,那么由于它只有一个许可证,所以只能允许一个线程通过,并且当之前的线程归还许可证后,会允许其他线程继续获得许可证。

其实这点和 ReentrantLock 很像,只有一个线程能获得锁,并且当这个线程释放锁之后,会允许其他的线程获得锁。那如果线程发现当前没有额外的许可证时,或者当前得不到锁,那么线程就会被阻塞,并且等到后续有许可证或者锁释放出来后,被唤醒,所以这些环节都是比较类似的。

为什么需要 AQS

锁和协作类,它们有很多工作是类似的,所以如果能把实现类似工作的代码给提取出来,变成一个新的底层工具类(或称为框架)的话,就可以直接使用这个工具类来构建上层代码了,而这个工具类其实就是 AQS

如果没有 AQS

如果没有 AQS,那就需要每个线程协作工具类自己去实现至少以下内容,包括:

  • 状态的原子性管理
  • 线程的阻塞与解除阻塞
  • 队列的管理

这里的状态对于不同的工具类而言,代表不同的含义,比如对于 ReentrantLock 而言,它需要维护锁被重入的次数,但是保存重入次数的变量是会被多线程同时操作的,就需要进行处理,以便保证线程安全。不仅如此,对于那些未抢到锁的线程,还应该让它们陷入阻塞,并进行排队,并在合适的时机唤醒。所以说这些内容其实是比较繁琐的,而且也是比较重复的,而这些工作目前都由 AQS 来承担了。

哪有什么岁月静好,不过是有人替你负重前行。

AQS 的作用

AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。

2、AQS 内部原理

AQS 最核心的三大部分就是状态队列期望协作工具类去实现的获取/释放等重要方法

state 状态

如果 AQS 想要去管理或者想作为协作工具类的一个基础框架,那么它必然要管理一些状态,而这个状态在 AQS 内部就是用 state 变量去表示的。

/**
 * The synchronization state.
 */
private volatile int state;

而 state 的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义

(1)Semaphore

在信号量里面,state 表示的是剩余许可证的数量。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,所以信号量的 state 相当于是一个内部计数器。

(2)CountDownLatch

CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门被打开。

(3)ReentrantLock

ReentrantLock 中它表示的是锁的占有情况。最开始是 0,表示没有任何线程占有锁;如果 state 变成 1,则就代表这个锁已经被某一个线程所持有了。

那为什么还会变成 2、3、4 呢?为什么会往上加呢?因为 ReentrantLock 是可重入的,同一个线程可以再次拥有这把锁就叫重入

如果这个锁被同一个线程多次获取,那么 state 就会逐渐的往上加,state 的值表示重入的次数。在释放的时候也是逐步递减,只有当它减到 0 的时候,此时恢复到最开始的状态了,则代表现在没有任何线程持有这个锁了。

state 等于 0 表示锁不被任何线程所占有,代表这个锁当前是处于释放状态的,其他线程此时就可以来尝试获取了。

state 并发修改问题

因为 state 是会被多个线程共享的,会被并发地修改,所以所有去修改 state 的方法都必须要保证 state 是线程安全的。可是 state 本身它仅仅是被 volatile 修饰的,volatile 本身并不足以保证线程安全

AQS 中两个修改 state 的方法源码:

protected final void setState(int newState) {
    state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

(1)当对基本类型的变量进行直接赋值时,如果加了 volatile 就可以保证它的线程安全。注意,这是 volatile 的非常典型的使用场景。

(2)利用了 Unsafe 里面的 CAS 操作,利用 CPU 指令的原子性保证了这个操作的原子性。

总结

AQS 中有 state 这样的一个属性,是被 volatile 修饰的,会被并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义。

FIFO 队列

FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。

假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。AQS 的一大功能就是充当线程的“排队管理器”。

当多个线程去竞争同一把锁的时候,就需要用排队机制把那些没能拿到锁的线程串在一起;而当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚刚释放的那把锁。

AQS 就一直维护这个队列,并把等待的线程都放到队列里面。

队列内部是双向链表的形式,其数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。

image-20220216145224479

在队列中,分别用 headtail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS 负责操作的。

image-20220216145338645

获取/释放方法

获取和释放相关的重要方法,这些方法是协作工具类的逻辑具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。

获取方法

获取操作通常会依赖 state 变量的值,根据 state 值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞。

(1)ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。

(2)Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态。

(3)CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门放开了,所以之前阻塞的线程就会被唤醒。

释放方法

获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。

(1)在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;

(2)在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。

3、AQS 应用原理

AQS 用法

如果想使用 AQS 来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK利用 AQS 类的主要步骤

  • 第一步,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS
  • 第二步,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquiretryRelease 等方法;如果是非独占,则重写 tryAcquireSharedtryReleaseShared 等方法;
  • 第三步,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquirerelease 等方法,非独占则调用 acquireSharedreleaseSharedacquireSharedInterruptibly 等方法。

为什么不以接口的方式实现,而是以重写的方式?

AQS 的原作者 Doug Lea 的论文中已经进行了说明,他认为如果是实现接口的话,那每一个抽象方法都需要实现

实际上我们并不是每个方法都需要重写,根据需求的不同,有选择的去实现一部分就足以了,所以就设计为不采用实现接口,而采用继承类并重写方法的形式。

相关方法源码,可以发现代码就直接抛出异常,所以是需要我们自己去重写逻辑的:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

AQS 在 CountDownLatch 的应用

在 CountDownLatch 里面有一个子类,该类的类名叫 Sync,这个类正是继承自 AQS

源码如下:

private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }

    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

Sync 类继承了 AQS,这正是第一步。

还重写了 tryAcquireShared 和 tryReleaseShared 方法,这是第二步。

构造函数

源码如下:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

小于 0 抛出异常,大于 0 是调用了 Sync 类的构造方法,源码为:

Sync(int count) {
    setState(count);
}

我们发现调用了 setState() 方法,这个方法其实是 AQS 的方法,源码为:

protected final void setState(int newState) {
    state = newState;
}

通过 CountDownLatch 构造函数将传入的 count 最终传递到 AQS 内部的 state 变量,给 state 赋值,state 就代表还需要倒数的次数。

这也验证了 AQSJUC 的基石。

getCount

该方法的作用是获取当前剩余的还需要“倒数”的数量。源码如下:

public long getCount() {
    return sync.getCount();
}

调用了 SyncreleaseShared 方法,源码为:

int getCount() {
    return getState();
}

最终还是调用了 AQSgetState 方法,源码为:

protected final int getState() {
    return state;
}

所以最终它获取到的就在 AQSstate 变量的值。

countDown

该方法其实就是 CountDownLatch 的“释放”方法,源码为:

public void countDown() {
    sync.releaseShared(1);
}

调用了 SyncreleaseShared 方法,源码为:

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

SynctryReleaseShared 方法源码如下:

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

doReleaseShared 方法,唤醒所有线程:

private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            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;
    }
}

await

方法是 CountDownLatch 的“获取”方法,调用 await 方法会把线程阻塞,直到倒数为 0 才能继续执行。

源码为:

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

Sync -> AQS,最终是调用了 AQSacquireSharedInterruptibly 方法:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared 方法源码:

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

拓展阅读

一行一行源码分析清楚AbstractQueuedSynchronizer

从ReentrantLock的实现看AQS的原理及应用