AQS是什么?

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器

AQS 就是一个抽象类,主要用来构建锁和同步器

AQS 是 Java 中实现各种同步器的基础框架,它提供了一套通用的机制来管理线程的等待和唤醒,从而实现对共享资源的安全访问。AQS 的核心在于同步状态的管理以及等待队列的维护,这使得它成为了 Java 并发编程中非常重要的工具。

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueue等等皆是基于 AQS 的。

下面是AQS整体类图结构:

image.png

我们可以通过上面类图关系看出AQS是一个抽象类,但是在其源码实现中并不存在任何抽象方法,这是因为AQS设计的初衷更倾向于作为一个基础组件,并不希望直接作为操作类对外输出,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等。

从设计模式角度来看,AQS采用的模板模式的模式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作及解锁操作,为什么这么做呢?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的具体逻辑实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法。

AQS 的原理是什么?

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

CLH 锁是对自旋锁的一种改进,是一个虚拟的双向链表,暂时获取不到锁的线程将被加入到该链表中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)

image.png

synchronized重量级锁底层的实现是基于ObjectMonitor对象中的计数器实现的,而在AQS中也存在着异曲同工之处。AQS 使用 int 成员变量 volatile修饰的 state 表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作。**state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况**

以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被封装成Node节点加入到一个同步队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后就将state - 1,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会唤醒 主调用线程,然后主调用线程就继续后余动作。

AQS 中的等待队列是如何实现的?

synchronized重量级锁底层的实现是基于ObjectMonitor对象中的计数器实现的,而在AQS中也存在着异曲同工之处,它内部通过一个用volatile关键字修饰的int类型全局变量state作为标识来控制同步状态。当状态标识state为0时,代表着当前没有线程占用锁资源,反之当状态标识state不为0时,代表着锁资源已经被线程持有,其他想要获取锁资源的线程必须进入同步队列等待当前持有锁的线程释放。AQS通过内部类Node构建FIFO(先进先出)的同步队列用来处理未获取到锁资源的线程,将等待获取锁资源的线程加入到同步队列中进行排队等待。同时AQS使用内部类ConditionObject用来构建等待队列,当Condition调用await()方法后,等待获取锁资源的线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁资源的竞争。值得我们注意的是在这里存在两种类型的队列:

①同步队列当线程获取锁资源发现已经被其他线程占有而加入的队列;
②等待队列(可能存在多个):当Condition调用await()方法后加入的队列;

大家在理解时不可将两者混为一谈。我们可以首先分析一下AQS中的同步队列,AQS同步队列模型如下:

1
2
3
4
5
6
7
8
9
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
// 指向同步队列的头部
private transient volatile Node head;
// 指向同步队列的尾部
private transient volatile Node tail;
// 同步状态标识
private volatile int state;
// 省略......
}

其中head以及tail是AQS的全局变量,其中head指向同步队列的头部,但是需要注意的是head节点为空不存储信息,而tail指向同步队列的尾部。AQS中同步队列采用这种方式构建双向链表结构方便队列进行节点增删操作。state则为我们前面所提到的同步状态标识,当线程在执行过程中调用获取锁的lock()方法后,如果state=0,则说明当前锁资源未被其他线程获取,当前线程将state值设置为1,表示获取锁成功。如果state=1,则说明当前锁资源已被其他线程获取,那么当前线程则会被封装成Node节点加入同步队列进行等待。Node节点是对每一个获取锁资源线程的封装体,其中包括了当前执行的线程本身以及线程的状态,如是否被阻塞、是否处于等待唤醒、是否中断等。每个Node节点中都关联着前驱节点prev以及后继节点next,这样能够方便持有锁的线程释放后能快速释放下一个正在等待的线程。Node类结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 标识线程已处于结束状态
static final int CANCELLED = 1;
// 等待被唤醒状态
static final int SIGNAL = -1;
// Condition条件状态
static final int CONDITION = -2;
// 在共享模式中使用表示获得的同步状态会被传播
static final int PROPAGATE = -3;

// 等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE四种
volatile int waitStatus;

// 同步队列中前驱结点
volatile Node prev;

// 同步队列中后继结点
volatile Node next;

// 获取锁资源的线程
volatile Thread thread;

// 等待队列中的后继结点(与Condition有关,稍后会分析)
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;
}
// 省略代码.....
}

在其中SHARED和EXCLUSIVE两个全局常量分别代表着共享模式和独占模式,共享模式即允许多个线程同时对一个锁资源进行操作,例如:信号量Semaphore、读锁ReadLock等采用的就是基于AQS的共享模式实现的。而独占模式则代表着在同一时刻只运行一个线程对锁资源进行操作,如ReentranLock等组件的实现都是基于AQS的独占模式实现。全局变量waitStatus则代表着当前被封装成Node节点的线程的状态。

一共存在五种情况:

  • 0 初始值状态:waitStatus=0,代表节点初始化。
  • CANCELLED 取消状态:waitStatus=1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消该Node的节点,其节点的waitStatus为CANCELLED,进入该状态后的节点代表着进入了结束状态,当前节点将不会再发生变化。
  • SIGNAL 信号状态:waitStatus=-1,被标识为该状态的节点,当其前驱节点的线程释放了锁资源或被取消,将会通知该节点的线程执行。简单来说被标记为当前状态的节点处于等待唤醒状态,只要前驱节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程执行
  • CONDITION 条件状态:waitStatus=-2,与Condition相关,被表示为该状态的节点处于等待队列中,节点的线程等待在Condition条件,当其他线程调用了Condition的signal()方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取竞争锁资源。
  • PROPAGATE 传播状态:waitStatus=-3,该状态与共享模式有关,在共享模式中,被标识为该状态的节点的线程处于可运行状态。

image.png

全局变量pre和next分别代表着当前Node节点对应的前驱节点和后继节点,thread代表当前被封装的线程对象。nextWaiter代表着等待队列中,当前节点的后继节点(与Condition有关稍后分析)。到这里其实我们对于Node数据类型的结构有了大概的了解了。总之,**AQS作为JUC的核心组件,对于锁存在两种不同的实现,即独占模式(如ReetrantLock)与共享模式(如Semaphore)**。但是不管是独占模式还是共享模式的实现类,都是建立在AQS的基础上实现,其内部都维持着一个队列,当试图获取锁的线程数量超过当前模式限制时则会将线程封装成一个Node节点加入队列进行等待。而这一系列操作都是由AQS帮我们完成,无论是ReetrantLock还是Semaphore,其实它们的绝大部分方法最终都是直接或间接的调用AQS完成的。

AQS组件

image.png

  • AbstractOwnableSynchronizer抽象类: 内部定义了存储当前持有锁资源线程以及获取存储线程信息方法。
  • AbstractQueuedSynchronizer抽象类: AQS指的就是AbstractQueuedSynchronizer的首字母缩写,整个AQS框架的核心类。内部以虚拟队列的形式实现了线程对于锁资源获取(tryAcquire)与释放(tryRelease),但是在AQS中没有对锁获取与锁释放的操作进行默认实现,具体的逻辑需要子类实现,这样使得我们在开发过程中能够更加灵活的运用它。
  • Node内部类: AbstractQueuedSynchronizer中的内部类,用于构建AQS内部的虚拟队列,方便于AQS管理需要获取锁的线程。
  • Sync内部抽象类: ReentrantLock的内部类,继承AbstractQueuedSynchronizer类并实现了其定义的锁资源获取(tryAcquire)与释放(tryRelease)方法,同时也定义了lock()方法,提供给子类实现。
  • NonfairSync内部类: ReentrantLock的内部类,继承Sync类,非公平锁的实现者。
  • FairSync内部类: ReentrantLock的内部类,继承Sync类,公平锁的实现者。
  • Lock接口: Java锁类的顶级接口,定义了一系列锁操作的方法,如:lock()、unlock()、tryLock等。
  • ReentrantLock: Lock锁接口的实现者,内部存在Sync、NonfairSync、FairSync三个内部类,在创建时可以根据其内部fair参数决定使用公平锁/非公平锁,其内部操作绝大部分都是基于间接调用AQS方法完成。

我们可以通过上面类图关系看出AQS是一个抽象类,但是在其源码实现中并不存在任何抽象方法,这是因为AQS设计的初衷更倾向于作为一个基础组件,并不希望直接作为操作类对外输出,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等。

从设计模式角度来看,AQS采用的模板模式的模式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作及解锁操作,为什么这么做呢?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的具体逻辑实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法。

这样做的好处是显而易见,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过加锁/解锁的逻辑不同,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可