第十一章 AQS

11.1 AQS简介

AQSAbstractQueuedSynchronizer的简称,即抽象队列同步器,从字面意思上理解:

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)队列存储数据;
  • 同步:实现了同步的功能。

那AQS有什么用呢?AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器,只要子类实现它的几个protected方法就可以了,在下文会有详细的介绍。

11.2 AQS的数据结构

AQS内部使用了一个volatile的变量state来作为资源的标识。同时定义了几个获取和改变state的protected方法,子类可以覆盖这些方法来实现自己的逻辑:

  1. getState()
  2. setState()
  3. compareAndSetState()

这三种操作均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。

而AQS类本身实现的是一些排队和阻塞的机制,比如具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。它内部使用了一个先进先出(FIFO)的双端队列,并使用了两个指针head和tail用于标识队列的头部和尾部。其数据结构如图:

img

但它并不是直接储存线程,而是储存拥有线程的Node节点。

11.3 资源共享模式

资源有两种共享模式,或者说两种同步方式:

  • 独占模式(Exclusive):资源是独占的,一次只能一个线程获取。如ReentrantLock。
  • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。

一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock

AQS中关于这两种资源共享模式的定义源码(均在内部类Node中)。我们来看看Node的结构:

  1. static final class Node {
  2. // 标记一个结点(对应的线程)在共享模式下等待
  3. static final Node SHARED = new Node();
  4. // 标记一个结点(对应的线程)在独占模式下等待
  5. static final Node EXCLUSIVE = null;
  6. // waitStatus的值,表示该结点(对应的线程)已被取消
  7. static final int CANCELLED = 1;
  8. // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
  9. static final int SIGNAL = -1;
  10. // waitStatus的值,表示该结点(对应的线程)在等待某一条件
  11. static final int CONDITION = -2;
  12. /*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
  13. static final int PROPAGATE = -3;
  14. // 等待状态,取值范围,-3,-2,-1,0,1
  15. volatile int waitStatus;
  16. volatile Node prev; // 前驱结点
  17. volatile Node next; // 后继结点
  18. volatile Thread thread; // 结点对应的线程
  19. Node nextWaiter; // 等待队列里下一个等待条件的结点
  20. // 判断共享模式的方法
  21. final boolean isShared() {
  22. return nextWaiter == SHARED;
  23. }
  24. Node(Thread thread, Node mode) { // Used by addWaiter
  25. this.nextWaiter = mode;
  26. this.thread = thread;
  27. }
  28. // 其它方法忽略,可以参考具体的源码
  29. }
  30. // AQS里面的addWaiter私有方法
  31. private Node addWaiter(Node mode) {
  32. // 使用了Node的这个构造函数
  33. Node node = new Node(Thread.currentThread(), mode);
  34. // 其它代码省略
  35. }

注意:通过Node我们可以实现两个队列,一是通过prev和next实现CLH队列(线程同步队列,双向队列),二是nextWaiter实现Condition条件上的等待线程队列(单向队列),这个Condition主要用在ReentrantLock类中。

11.4 AQS的主要方法源码解析

AQS的设计是基于模板方法模式的,它有一些方法必须要子类去实现的,它们主要有:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

这些方法虽然都是protected方法,但是它们并没有在AQS具体实现,而是直接抛出异常(这里不使用抽象方法的目的是:避免强迫子类中把所有的抽象方法都实现一遍,减少无用功,这样子类只需要实现自己关心的抽象方法即可,比如 Semaphore 只需要实现 tryAcquire 方法而不用实现其余不需要用到的模版方法):

  1. protected boolean tryAcquire(int arg) {
  2. throw new UnsupportedOperationException();
  3. }

而AQS实现了一系列主要的逻辑。下面我们从源码来分析一下获取和释放资源的主要逻辑:

11.4.1 获取资源

获取资源的入口是acquire(int arg)方法。arg是要获取的资源的个数,在独占模式下始终为1。我们先来看看这个方法的逻辑:

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

首先调用tryAcquire(arg)尝试去获取资源。前面提到了这个方法是在子类具体实现的。

如果获取资源失败,就通过addWaiter(Node.EXCLUSIVE)方法把这个线程插入到等待队列中。其中传入的参数代表要插入的Node是独占式的。这个方法的具体实现:

  1. private Node addWaiter(Node mode) {
  2. // 生成该线程对应的Node节点
  3. Node node = new Node(Thread.currentThread(), mode);
  4. // 将Node插入队列中
  5. Node pred = tail;
  6. if (pred != null) {
  7. node.prev = pred;
  8. // 使用CAS尝试,如果成功就返回
  9. if (compareAndSetTail(pred, node)) {
  10. pred.next = node;
  11. return node;
  12. }
  13. }
  14. // 如果等待队列为空或者上述CAS失败,再自旋CAS插入
  15. enq(node);
  16. return node;
  17. }
  18. // 自旋CAS插入等待队列
  19. private Node enq(final Node node) {
  20. for (;;) {
  21. Node t = tail;
  22. if (t == null) { // Must initialize
  23. if (compareAndSetHead(new Node()))
  24. tail = head;
  25. } else {
  26. node.prev = t;
  27. if (compareAndSetTail(t, node)) {
  28. t.next = node;
  29. return t;
  30. }
  31. }
  32. }
  33. }

上面的两个函数比较好理解,就是在队列的尾部插入新的Node节点,但是需要注意的是由于AQS中会存在多个线程同时争夺资源的情况,因此肯定会出现多个线程同时插入节点的操作,在这里是通过CAS自旋的方式保证了操作的线程安全性。

OK,现在回到最开始的aquire(int arg)方法。现在通过addWaiter方法,已经把一个Node放到等待队列尾部了。而处于等待队列的结点是从头结点一个一个去获取资源的。具体的实现我们来看看acquireQueued方法

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. // 自旋
  6. for (;;) {
  7. final Node p = node.predecessor();
  8. // 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
  9. if (p == head && tryAcquire(arg)) {
  10. // 拿到资源后,将head指向该结点。
  11. // 所以head所指的结点,就是当前获取到资源的那个结点或null。
  12. setHead(node);
  13. p.next = null; // help GC
  14. failed = false;
  15. return interrupted;
  16. }
  17. // 如果自己可以休息了,就进入waiting状态,直到被unpark()
  18. if (shouldParkAfterFailedAcquire(p, node) &&
  19. parkAndCheckInterrupt())
  20. interrupted = true;
  21. }
  22. } finally {
  23. if (failed)
  24. cancelAcquire(node);
  25. }
  26. }

这里parkAndCheckInterrupt方法内部使用到了LockSupport.park(this),顺便简单介绍一下park。

LockSupport类是Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:

  • park(boolean isAbsolute, long time):阻塞当前线程
  • unpark(Thread jthread):使给定的线程停止阻塞

所以结点进入等待队列后,是调用park使它进入阻塞状态的。只有头结点的线程是处于活跃状态的

当然,获取资源的方法除了acquire外,还有以下三个:

  • acquireInterruptibly:申请可中断的资源(独占模式)
  • acquireShared:申请共享模式的资源
  • acquireSharedInterruptibly:申请可中断的资源(共享模式)

可中断的意思是,在线程中断时可能会抛出InterruptedException

总结起来的一个流程图:

acquire流程

11.4.2 释放资源

释放资源相比于获取资源来说,会简单许多。在AQS中只有一小段实现。源码:

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }
  10. private void unparkSuccessor(Node node) {
  11. // 如果状态是负数,尝试把它设置为0
  12. int ws = node.waitStatus;
  13. if (ws < 0)
  14. compareAndSetWaitStatus(node, ws, 0);
  15. // 得到头结点的后继结点head.next
  16. Node s = node.next;
  17. // 如果这个后继结点为空或者状态大于0
  18. // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
  19. if (s == null || s.waitStatus > 0) {
  20. s = null;
  21. // 等待队列中所有还有用的结点,都向前移动
  22. for (Node t = tail; t != null && t != node; t = t.prev)
  23. if (t.waitStatus <= 0)
  24. s = t;
  25. }
  26. // 如果后继结点不为空,
  27. if (s != null)
  28. LockSupport.unpark(s.thread);
  29. }

参考资料