Java 并发之 AbstractQueuedSynchronizer

如果你读过 JUC 中 ReentrantLock、CountDownLatch、FutureTask、Semaphore 等的源代码,会发现其中都有一个名为 Sync 的类,而这个类是以 AbstractQueuedSynchronizer 为基础的,所以说 AbstractQueuedSynchronizer 是 JUC 的基础之一(注:CyclicBarrier 并没有直接以 AQS 为基础)。出于知其然也要知其所以然的目的,我学习了 AQS 的实现原理,并总结成此文。

数据结构

在 AQS 中,有两个重要的数据结构,一个是 volatile int state,另一个是 class Node 组成的双向链表。

int state

顾名思义,这个变量是用来表示 AQS 的状态的,例如 ReentrantLock 的锁的状态和重入次数、FutureTask 中任务的状态、CountDownLatch 中的 count 计数等等。这个值的更新都是由 AQS compareAndSetState 方法来实现的,而这个方法则是通过 Compare and Swap 算法实现,至于这个算法的细节就不多说了。在 JDK 中,这个算法是由 Native 方法实现的。

Node 双向链表

Node 是 AQS 的一个内部类,主要有 waitStatus、prev、next、thread 等这么几个属性。不介绍,从名字大家也能知道这些属性的用途。

head

指向 Node 链表的头部。可为空、一个没用引用线程对象的空 Node、一个引用当前占有 AQS 的线程对象的 Node。

tail

指向 Node 链表的尾部。

工作流程

acquire

这个方法本身的代码并不长,但是流程说起来也不简单。

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

首先尝试调用 tryAcquire(int) 方法来获取(锁等等,具体获取什么取决于你将 AQS 应用在何种场景中)。tryAcquire 这个方法是抽象方法,具体行为需要由子类来实现。在 ReentrantLock 内部类 Sync 实现中,这个方法通过 CAS 算法设置锁的状态,用 AQS 中的 state 表示锁被重入的次数。

如果 tryAcquire 成功了,那也就没什么了,整个 acquire 操作也就成功了。如果 tryAcquire 失败,那就需要把当前线程做入队操作。这个入队操作是由 Node addWaiter(Node mode) 方法来实现的。这个方法做的事情并不复杂,就是将当前线程(因为它没有 acquire 成功)放入队列的尾部。如果队列是空的,则在做入队操作之前先初始化队列。队列的头节点并不引用任何线程对象或者其引用的线程对象获取的当前的这个 AQS。总之,head 中的线程对象引用都是没有被挂起的(null 自然不会被挂起)。

在入队操作成功之后,会再对刚刚入队的线程做一次 acquire 操作。这样做的目的是为了应对短暂竞争的场景,尽量避免挂起线程的操作。

final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

上面这段代码就是让已入队的线程对象做 acquire 操作。p.next = null 的目的在于当 node 所引用的节点需要回收时加快内存回收的速度。

如果刚入队的节点没有 acquire 成功,那这个 Node (其实是这个 Node 所引用的线程) 十有八九将被挂起。判断的条件是这个 Node 的 waitStatus,这个状态必须设成 SIGNAL,就是告诉别人我要被挂起了,等你们 release 的时候记得叫一下兄弟。如果状态等于0,那就把状态设置成 SIGNAL。这之后便把当前线程挂起,再然后自然就没有然后了,直到被 release。

release

接下来再说说 release 的过程。release 的过程相对简单,和 acquire 类似,首先进行 tryRelease 操作。还是以 ReentrantLock 为例,tryRelease 会首先判断当前线程是否 acquire 了 AQS,如果是,则改变 AQS 的状态。然后在尝试恢复一个被挂起的线程,通常是 head 的 next 节点所引用的线程对象。

State

在 AQS 中有一个 int 类型的 volatile 变量 state,使用 AQS 的类可以自定义 state 对其的含义。例如,ReentrantLock 用 0 表示没有线程获取锁,大于 0 则表示重入锁的重入次数;Semaphore 用来表示许可数量;FutureTask 用来表示任务状态,例如运行中、已完成等。

在扩展 AQS 时,子类需要根据自己的需求在诸如 tryAcquire 方法中使用 compareAndSetState 方法设置相应的状态值。

独占模式和共享模式

处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。

上面这段话是摘自 JDK API。说实话,这段话说的很不明白,只看这段话我也没明白,所以还是去看这两个模式如何在实际中去应用。以采用 Shared 模式使用 AQS 的 CountDownLatch 为例,它采用 acquireShared 和 releaseShared 作为其业务方法。如同 acquire 和 release 这两个方法,acquireShared 和 releaseShared 也会调用 tryAcquireShared 和 tryReleaseShared 这两个需要由子类实现的方法。

以 CountDownLatch 为例,它的 tryAcquireShared 的实现如下:

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

是不是非常简单。对比一下 ReentrantLock 的 tryAcquire 方法的实现,我这里就不贴出源代码了。但即使不看源代码,我们也知道,ReentrantLock 的 tryAcquire 方法是排他的。但是看一下 CountDownLatch 的 tryAcquireShared 方法的实现,完全看不出排他性的体现。其实稍加注意就会发现,tryAcquire 和 tryAcquireShared 的方法定义存在一个巨大的不同,就是返回值的不同。tryAcquire 返回的是 boolean 类型,其分别表示 acquire 成功或失败,而 tryAcquireShared 返回的却是 int 类型,负、零、正代表三种含义:失败、独占获取、共享获取。这就是 AQS 文档中对独占模式和共享模式描述中的那段“可能(但不是一定)”的原因。

通过阅读 CountDownLatch 的源代码和我上面的讲解,我想大部分人应该都能理解 AQS 独占模式和共享模式的含义了。

参考资料

  • Inside AbstractQueuedSynchronizer
  • The j.u.c Synchronizer Framework翻译
  • AbstractQueuedSynchronizer的介绍和原理分析

你可能感兴趣的:(Java 并发之 AbstractQueuedSynchronizer)