万字长文分析 AQS 原理以及应用

万字长文分析 AQS 原理以及应用_第1张图片

1 、引言

本文可能又臭又长,希望可以尽量将AQS相关的内容叙述清楚(个人能力有限),不喜勿喷(标题是假的,标题党)。

AQS,即 juc 并发包下的 AbstractQueuedSynchronizer,我们也可以叫做抽象队列同步器。其实现了一种基于队列的阻塞锁以及相关的同步器,AQS 可以用作单个int变量表示 state(状态)的同步器的基准。我们熟悉的一些并发工具 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 都是基于 AQS 实现的。AQS 定义了同步相关的一些公共 API 方法,以及预留了一些protect方法供子类实现(子类只要使用就必须实现),AQS 并不负责状态变更维护以及同步机制,对于不同的子类实现,其状态量state所表示的含义不同,子类通过实现 protect 方法达到原子性的变更状态量 state 以及阻塞释放等同步机制。

前面是 AQS 一段极简的介绍,后面我们会一步一步将其拆解,本文想达到的目的—通过本文你可以:

  • 理解 AQS 数据结构及算法思想

  • 熟悉 AQS 的 API 及其分层实现

  • 理解 AQS 的不同运用示例,比如 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch

  • 可以自定义自己的 AQS 来实现多线程下的同步以保障线程安全

学习本文需要的预备知识很简单,你需要知道:

  • queue (队列)的数据结构及思想

  • CAS (比较并交换)的思想和原理

  • 线程生命周期及相关 API 支持

2 、AQS 概述

2.1 state-AQS 同步状态量

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

首先,AQS 维护了一个 volatile 的状态变量 state,该变量表示同步器的状态维护基于一个 int 值,至于这个 state 值的含义,从抽象意义来说它就是同步状态(废话)。具体来说,所有可以基于一个 int 值来维护线程对共享资源的持有状态(当然也结合其他的 API )的组件都可以基于 AQS 实现,比如:

  • 对于 ReentrantLock 中实现的 AQS 子类 Sync,state 表示线程加锁次数(0 表示未加锁,否则表示被同一个线程加锁了多少次-可重入性)

  • 对于 ReentrantReadWriteLock 来说,state 的高 16 位表示线程对读锁的加锁次数,低 16 位表示线程对写锁的加锁次数(当然读锁、写锁也分别是可重入的,并且会有互斥性,这些后面详细说明)

  • 对于 Semaphore 来说,state 表示其可用信号量,简短不严谨的说法可以是:state 的数值表示其还可以被线程获取的次数,0 表示信号量已经被耗尽(暂不可用)

  • 对于 CountDownLatch 来说,state 表示锁闩需要被解锁的次数 (release),可以实现多个线程执行 release 动作后 state 变为 0 标识该锁闩被打开(对应阻塞的线程此时才能被释放执行),进而通过计数器实现多个线程前置动作全部完成后才能执行后续动作的作用

可以通过前面的示例可以看出来,AQS 中的 state 表示同步状态,其具体含义及维护由子类实现,我们可以非严谨的以下图表示:

万字长文分析 AQS 原理以及应用_第2张图片

2.2 AQS- 它竟然是个队列

上一小节我们提到了 state- 这个变量实现了 AQS 的从资源状态同步量的维度,这一小节,我们从另外一个维度看一下,那就是 AQS 本身竟然(不要问为什么是竟然,没有为什么,标题党)是一个队列。我们来看一下 AQS 和队列相关的一些变量及组件(省略了一些无关的注释以及 API 方法):

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  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;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
    		
    	volatile int waitStatus;
    	volatile Node prev;
    	volatile Node next;
        volatile Thread thread;
    	Node nextWaiter;
    	Node() {}
    	// ......
    	private static final VarHandle NEXT;
        private static final VarHandle PREV;
        private static final VarHandle THREAD;
        private static final VarHandle WAITSTATUS;
        static {
            try {
                MethodHandles.Lookup l = MethodHandles.lookup();
                NEXT = l.findVarHandle(Node.class, "next", Node.class);
                PREV = l.findVarHandle(Node.class, "prev", Node.class);
                THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
                WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
            } catch (ReflectiveOperationException e) {
                throw new ExceptionInInitializerError(e);
            }
        }
  }
  
  private transient volatile Node head;
  private transient volatile Node tail;
  private volatile int state;
  // ......
}

我们可以看出,AQS 定义了静态内部类 Node (充当队列元素的角色),并且持有两个 Node 的引用 head、tail,就是个常规的队列也没啥好说的。Node 内定义了其

  • prev (前驱节点) next (后继节点)

  • thread (与当前节点关联的线程)

  • waitStatus (当前队列节点的等待状态,不同的状态有不同的含义,可能的状态可以上面的一些静态变量如 SIGNAL 等),waitStatus 与锁的抢占以及状态同步相关,后面我们会结合源码详细展开

  • SHARED、EXCLUSIVE,简单的 Node 实例,只是用来表示占有模式(共享或独占)

  • nextWaiter,表示下一个等待节点(这个是和 Condition 相关的,对目前的讨论没有影响,后面会展开讨论)

  • 一些变量原子操作变量如 NEXT 用来原子操作 next,实现 Node 内变量的 CAS 原子操作

所以 AQS 的数据结构看起来是这个样子的:

万字长文分析 AQS 原理以及应用_第3张图片

可以看出来 AQS 维护了一个双向队列,每个队列节点中维护各自相关的 thread、waitStatus、nextWaiter (暂时忽略)等。

2.3 AbstractOwnableSynchronizer

上个小节我们可以看出来 AQS 实现了 AbstractOwnableSynchronizer,因此也持有了 exclusiveOwnerThread 来表示一个独占的线程引用,AbstractOwnableSynchronizer 也非常简单:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    protected AbstractOwnableSynchronizer() { }
    // 独占模式下拥有该同步器的当前线程
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

上个小节我们看出来 AQS 本身每个队列 Node 都会维护各自的 thread,这些队列中的线程可能是不同的状态,其状态转换、阻塞、恢复会用到队列的排队机制,而 AbstractQueuedSynchronizer 作为整体协调线程同步的上层门面,在独占(排他)模式下也会持有一个 exclusiveOwnerThread 表示当前独占的线程,这个也是很好理解的。

2.4 AQS 数据结构总结

前面三个小节,我们可以知道 AQS 维护了以下核心数据:

  • 表示同步器状态的 state,其具体含义及变更维护由子类实现

  • 用于线程阻塞排队、状态变更的同步队列(持有 head,tail,元素为 Node 类型)

  • 同步队列元素类型 Node,其维护了与之相关的 thread、waitStatus、nextWaiter、模式等

  • 通过实现 AbstractOwnableSynchronizer 来实现获取当前独占同步器的 thread(null 表示没有线程独占同步器)

既然清楚了 AQS 的数据结构支撑,下面的部分我们就开始进入 AQS 的 API 方法,了解一下 AQS 是如何基于其数据结构及 API 配合实现线程同步的。

3 、AQS 的核心 API 及扩展

3.1 AQS 已经提供的能力

结合 AbstractQueuedSynchronizer 的源码来看,其大部分方法都是 public final 或者 private实现的,这也表明了这些方法不可以被重写,这些 API 表示 AQS 暴露出去的公共能力,比如获取资源、释放资源的入口以及提供的入队等候等,在列出这些 API 之前,我们先就以排他模式下的获取看一下(只需看方法签名及签名注释,方法内代码先不用看,避免引入困惑):

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  // ...
  
  // 独占模式获取(忽略中断)
  public final void acquire(int arg) {
    // 1 加锁成功
    // 2 加锁失败后 将当前线程封装为模式为EXCLUSIVE的Node节点入队
    // 3 入队后的节点排队等候state可用 在抢占成功(或异常)之前不断自旋尝试(伴随阻塞等待)
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
  }
  
  protected boolean tryAcquire(int arg) {
    // AQS自身不支持该操作
    throw new UnsupportedOperationException();
  }
  
  // 将与线程相关的Node入队
  private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
      Node oldTail = tail;
      if (oldTail != null) {
        node.setPrevRelaxed(oldTail);
        if (compareAndSetTail(oldTail, node)) {
          oldTail.next = node;
          return node;
        }
      } else {
        initializeSyncQueue();
      }
    }
  }
  
  // 入队后的Node尝试【获取-失败阻塞-被通知再获取】自旋直至换取成功或者异常退出
  final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
      // 自旋,要么获取锁,要么中断
      for (;;) {
        // 如果当前节点的前驱节点为头节点 说明当前节点是队列的最前面有效节点(头节点是虚节点) 就尝试获取锁
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {
          // 获取锁成功后 将当前节点设置为头节点(虚节点)
          setHead(node);
          p.next = null; // help GC
          return interrupted;
        }
        // 进行到这里说明p为头节点且当前节点没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,
        // 这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源
        if (shouldParkAfterFailedAcquire(p, node))
          interrupted |= parkAndCheckInterrupt();
      }
    } catch (Throwable t) {
      // 
      cancelAcquire(node);
      if (interrupted)
        selfInterrupt();
      throw t;
    }
  }
}

上面以 acquire 为入口,列出了相关的 API 方法,以 aquire 为例是因为 AQS 的各种应用比如 ReentrantLock 执行加锁 lock() 实际上就是调用其 Sync(AQS 的子类)的 acquire(1) 来实现加锁的,同理譬如 acquireInterruptibly、tryAcquireNanos、release、acquireShared、releaseShared 等(先不用管这些方法,现在可以见名知义即可)这些 public 的门面方法也是实现不同操作的入口。为了不引入未知的迷惑,这里再以 ReentrantLock 为例来看一下 aquire 方法的使用(省略了尽量多的无关代码):

public class ReentrantLock implements Lock, java.io.Serializable {
  private final Sync sync;
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    final boolean nonfairTryAcquire(int acquires) {
      // ... 省略巴啦吧啦
    }
    
  }
  
  static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
      return nonfairTryAcquire(acquires);
    }
  }
  // ...
  
  public ReentrantLock() {
    sync = new NonfairSync();
  }
  
  public void lock() {
    sync.acquire(1);
  }
}

通过 ReentrantLock 的 lock() 我们可以看出来,其本身是利用了 AQS 提供的能力 sync.acquire(1) 来做的,正如我们前面所说了(但其 Sync 实现了 AQS 的 protect 方法来实现自己的逻辑)。那么关于 AQS 的使用入口暂时谈到这里就好了,下面我们继续回到 AQS 的公共 API 上。

通过我贴的 AQS 源码,以 acquire(int arg) 为例,列出了一些内部调用相关的方法,这些方法大多数都是 AQS 已经实现的比如 addWaiter(Node mode),acquireQueued(final Node node, int arg) 等(当然没有列出所有内部递归调用的方法,那也不可能对吧,还是要自己看源码),除了 tryAcquire(int arg),这个方法却是一个 protect 修饰的,且会抛出 UnsupportedOperationException。而在 ReentrantLock的NonfairSync (非公平同步器)中我们能找到其对 AQS 的 tryAcquire(int acquires) 实现:

protected final boolean tryAcquire(int acquires) {
  return nonfairTryAcquire(acquires);
}

至此,AQS 的公共能力 API 的举例就完了,AQS 对同步器大部分动作已经做了实现,子类可以复用(也必须复用,因为都是 final 或 private) 这些方法,然后子类需要实现 AQS 预定义的一些未支持的 protect 方法比如 tryAcquire(int arg) 等(说是模版模式也算不上,就是个面向对象的特性吧),这也体现了 JDK 对于 AQS 的 API 的一种分层思想:

万字长文分析 AQS 原理以及应用_第4张图片

上面这张图是我自己对 AQS 的 API 应用、实现、耦合关系的一个理解,上面的分层并不是严格的,只是我个人的一个归纳以及喜好,大家看网上的相关文章或者自己的理解可能是不一样的,这个我觉得应该很正常。

通过上图我们可以看出,AQS 自身对于暴露 API、获取/释放 state 流程、用作支持的队列操作、state 操作、以及一些辅助的查询方法是已经定义并且实现好了,这些是 AQS 自身已经实现的能力。当然,要保证 AQS 的 API 完备可用的另外一个条件就是 AQS 应用层需要实现自己要用到的方法,并且这种实现要符合 AQS 的规范。

3.2 AQS 的扩展点

通过上一小节,我们可以看出来 AQS 自身已经定义了基于队列的同步器获取/释放同步状态 state 的入口、自旋操作、队列操作实现、state 操作等,但有一点需要明确,那就是 AQS 本身并不负责维护真正意义上的 state 获取、释放的含义以及方式,而是把其留给子类实现。

换句话说,对 AQS 来说, API 入口的 acquire(int arg) 只是暴露给上层使用,表示获取同步状态 state,但其又是通过调用 tryAcquire 成功或者失败后进行入队、排队等候、不断自旋tryAcquire 来达到最终获取到 state 的目的。这些入口、流程、入队、自旋尝试、state 操作能力是 AQS 实现好的,但究竟获取 state 这个动作如何定义(是 state 递增还是递减或者其他)、以及获取之前是否要排队(公平/非公平)、独占或者共享的操作是由 AQS 的子类实现的。子类可以根据自己的需要实现独占或者共享以及自己的state含义、操作方式。类似的方法有以下几种:

  • 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();}

  • protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }

以上这些方法是 AQS 中唯一需要子类实现的方法,当然不是必须全部实现的,子类结合自己的需求实现,比如独占模式的 ReentrantLock 实现了 tryAcquire、tryRelease、isHeldExclusively,而 ReentrantReadWriteLock 实现了以上全部 5 个方法(因为读锁共享、写锁独占且读写锁互斥),而只支持共享模式的 Semaphore、CountDownLatch 只实现了共享模式需要用到的 tryAcquireShared、tryReleaseShared 两个方法。

4 、让 AQS 动起来-以 ReentrantReadWriteLock 为例分析 AQS 的数据结构及算法实现

4.1 ReentrantReadWriteLock 概览

前面几节啰里八嗦一大堆总算是把 AQS 的数据结构以及 API 的支撑讲完了,总体来说还是比较泛化的,其目的是希望能够从全局上理解 AQS 实现的原理以及思想。从这一节开始,我们就以 ReentrantReadWriteLock 为例,来让 AQS 动起来,看一下这玩意到底是怎么做到前面说的那些功能的。之所以选择 ReentrantReadWriteLock 为例,是因为其同时实现了 AQS 的独占、共享两种模式,同时还支持可重入的公平或非公平的加锁,所以 ReentrantReadWriteLock 理解了,那什么 ReentrantLock、Semaphore、CountDownLatch 也自然很简单了,直接将 ReentrantReadWriteLock 的某个子集类比即可。

首先看一下 ReentrantReadWriteLock 对我们有用的代码:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
  /** 读锁 */
  private final ReentrantReadWriteLock.ReadLock readerLock;
  /** 写锁 */
  private final ReentrantReadWriteLock.WriteLock writerLock;
  /** 同步器 */
  final Sync sync;
  
  public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
  }
  
  public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
  public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    /** 获取读锁加锁次数 state右移16位得到高16位 */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** 获取写锁加锁次数 取state低16位 */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    
    // ...省略一大堆代码 后面看
  }
  
  /** 非公平锁同步器 */
  static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
      return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
      return apparentlyFirstQueuedIsExclusive();
    }
  }
  
  /** 公平锁同步器 */
  static final class FairSync extends Sync {
    final boolean writerShouldBlock() {
      return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
      return hasQueuedPredecessors();
    }
  }
  
  /** 内部类-读锁 */
  public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected ReadLock(ReentrantReadWriteLock lock) {
      sync = lock.sync;
    }
    // 加读锁
    public void lock() {
      sync.acquireShared(1);
    }
    // 释放读锁
    public void unlock() {
      sync.releaseShared(1);
    }
    // ... 省略
  }
  
  /** 内部类-写锁 */
  public static class WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
      sync = lock.sync;
    }
    // 加写锁
    public void lock() {
      sync.acquire(1);
    }
    // 释放写锁
    public void unlock() {
      sync.release(1);
    }
  }
  // ... 省略
}

好了,ReentrantReadWriteLock 的主干代码都在这里了,可以看出来并不多(省略了 Sync 的实现以及一些和 lock、unlock 类似的方法比如 tryLock 等),毕竟 ReentrantReadWriteLock 同时实现了读锁、写锁才 1500 行(这其中大部分还是注释以及读写锁入口类似的代码),而 AbstractQueuedSynchronizer 却有将近 2500 行,而只实现了共享模式的 Semaphore700 行、CountDownLatch300 行,所以 AbstractQueuedSynchronizer 才是核心同时又提供了方便的扩展点。

通过上面的源码及结合我们平时自己使用可以看出来,ReentrantReadWriteLock 的读锁、写锁其实就是两把锁(两个类),但它们使用相同的同步器 Sync,只不过区分公平和非公平的同步器罢了(在公平与否确定下来后读写锁使用的还是一个同步器)。因为 ReadLock、WriteLock 公用一个同步器 Sync,所以 state 状态量需要同时管理写锁和读锁,JDK 使用 state 的高 16 位表示读锁持有的状态量,低 16 位表示写锁持有的状态量,然后对外暴露两把锁,内部通过 Sync 的控制逻辑将读写锁联系起来并且做到可重入、读写锁互斥、读锁不互斥等。

4.2 ReentrantReadWriteLock 之 WriteLock 使用 — AQS 独占(排他)模式

随便写一个 ReentrantReadWriteLock 的使用作为我们分析的入口吧:

public static void main(String[] args) {
  ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
  ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
  ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
  for (int i = 0; i < 3; i++) {
    new Thread(() -> {
      try {
        writeLock.lock();
        System.out.println(Thread.currentThread() + "加写锁成功");
        TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        System.out.println(Thread.currentThread() + "释放写锁");
        writeLock.unlock();
      }
    }).start();
  }
  for (int i = 0; i < 3; i++) {
    new Thread(() -> {
      try {
        readLock.lock();
        System.out.println(Thread.currentThread() + "加读锁成功");
        TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        System.out.println(Thread.currentThread() + "释放读锁");
        readLock.unlock();
      }
    }).start();
  }
}

对应输出如下:

Thread[Thread-1,5,main]加写锁成功
Thread[Thread-1,5,main]释放写锁
Thread[Thread-2,5,main]加写锁成功
Thread[Thread-2,5,main]释放写锁
Thread[Thread-0,5,main]加写锁成功
Thread[Thread-0,5,main]释放写锁
Thread[Thread-4,5,main]加读锁成功
Thread[Thread-3,5,main]加读锁成功
Thread[Thread-5,5,main]加读锁成功
Thread[Thread-5,5,main]释放读锁
Thread[Thread-3,5,main]释放读锁
Thread[Thread-4,5,main]释放读锁

当然输出应该不止这一种(但本地最容易出现的就是这种),但有一点是确定的,在写锁释放之前,其他线程写锁以及读锁不可能加锁成功。即读写锁互斥,写锁互斥,但读锁不互斥。基于这个大家都知道的结论,我们由这个 demo 开始跟随源码。

首先 WriteLock 的 lock 很简单:

 public void lock() { sync.acquire(1);}

而前面我们已经提到了 acquire(int arg) 是 AQS 的公共 API,如下:

// 独占模式获取(忽略中断)
public final void acquire(int arg) {
  // 1 加锁成功
  // 2 加锁失败后 将当前线程封装为模式为EXCLUSIVE的Node节点入队
  // 3 入队后的节点排队等候state可用 在抢占成功(或异常)之前不断自旋尝试(伴随阻塞等待)
  if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

acquire 的逻辑是通用的:

万字长文分析 AQS 原理以及应用_第5张图片

上面的流程图展示了 AQS 执行 acquire 动作时的通用逻辑,我们逐项分析:

4.2.1 ReentrantReadWriteLock AQS 实现的 tryAcquire(int acquires)

之前我们提到了 ReentrantReadWriteLock 中实现的 AQS(Sync) 同时实现了独占模式和共享模式,其中独占模式(写锁应用)对应 AQS 的获取与释放为 aqs.acquire(1)/aqs.release(1),而共享模式(读锁应用)对应 AQS 的获取与释放为 aqs.acquireShared()1/aqs.releaseShared(1),如下所示:

Lock API AQS API 模式
WriteLock.lock() sync.acquire(1) 独占(排他)
WriteLock.unlock() sync.release(1) 独占(排他)
ReadLock.lock() sync.acquireShared(1) 共享
ReadLock.unlock() sync.releaseShared(1) 共享

这一小节我们先讨论写锁下的独占模式,writeLock.lock() 后调用 sync.acquire(1),对应逻辑如下:

public final void acquire(int arg) {
  // 1 加锁成功
  // 2 加锁失败后 将当前线程封装为模式为EXCLUSIVE的Node节点入队
  if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

首先就执行尝试获取 state 逻辑 tryAcquire(1),这里也是 ReentrantReadWriteLock 中 Sync 自己实现的获取逻辑(对应我们之前提到的,对state的含义及更改作出实现),并不是 AQS 暴露的公共 API 的实现:

protected final boolean tryAcquire(int acquires) {
  /*
   * Walkthrough:
   * 1. If read count nonzero or write count nonzero
   *    and owner is a different thread, fail.
   * 2. If count would saturate, fail. (This can only
   *    happen if count is already nonzero.)
   * 3. Otherwise, this thread is eligible for lock if
   *    it is either a reentrant acquire or
   *    queue policy allows it. If so, update state
   *    and set owner.
   */
  Thread current = Thread.currentThread();
  int c = getState();
  int w = exclusiveCount(c);
  if (c != 0) {
    // (Note: if c != 0 and w == 0 then shared count != 0)
    if (w == 0 || current != getExclusiveOwnerThread())
      return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
      throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() ||
      !compareAndSetState(c, c + acquires))
    return false;
  setExclusiveOwnerThread(current);
  return true;
}

上面这段代码即 ReentrantReadWriteLock 中 Sync (继承了 AQS )中 tryAuire 的逻辑,对应的是写锁(独占模式)排队之前(当然排队后也会走这段)的加锁逻辑,源码中的注释已经非常清楚,总结下来如下图所示:

万字长文分析 AQS 原理以及应用_第6张图片

当独占模式 acquire 时,一般第一步调用 tryAcquire 尝试获取 state 状态量,在 ReentrantReadWriteLock 中对应的实现就像上图所示。tryAcquire 的结果分为两种:

  • true,获取 state 成功,上层的 acquire 响应也成功并结束,对应线程继续执行

  • false,获取 state 失败,上层 acquire 开始执行包装线程排队等待流程

4.2.2 AQS 排队等待

上一小节提到独占模式如果 aqs.acquire 中 tryAcquire 失败,就会进入排队等待流程,这里再贴一下源码:

public final void acquire(int arg) {
  // 1 加锁成功
  // 2 加锁失败后 将当前线程封装为模式为EXCLUSIVE的Node节点入队
  if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

排队抢占 state 分为两步,第一步即将当前线程封装为排他模式的 Node 节点入队,对应的源码如下:

// Node构造器 为Node内的代码
Node(Node nextWaiter) {
  this.nextWaiter = nextWaiter;
  THREAD.set(this, Thread.currentThread());
}

// aqs代码 将Node入队 如果队列为空则初始化队列头尾节点为node 否则将node追加到队尾
private Node addWaiter(Node mode) {
  Node node = new Node(mode);

  for (;;) {
    Node oldTail = tail;
    // 添加到队尾
    if (oldTail != null) {
      node.setPrevRelaxed(oldTail);
      if (compareAndSetTail(oldTail, node)) {
        oldTail.next = node;
        return node;
      }
    } else {
      // 同时初始化队头与队尾
      initializeSyncQueue();
    }
  }
  
  private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
      tail = h;
  }
}

结合这段代码,如果一个线程来加写锁时之前已经有人加了读锁或者其他线程已经加了写锁, tryAquire 失败后,其就会进入同步队列,我们就以其他线程加了写锁为例,那么此时的 aqs 看起来应该是这样的:

万字长文分析 AQS 原理以及应用_第7张图片

此时,aqs 被 thread-1 独占(之前加了写锁),thread-2 进入同步队列等待,其 Node 的 waitStatus = 0 (默认初始化即为 0),aqs 的同步队列 head 指向一个虚节点(初次进入 for 循环初始化的节点),tail 指向 thread-2 对应的节点。

4.2.2 AQS 排队获取

上个小节我们已经完成了获取不到写锁的线程进入同步队列的操作,那么结合源代码我们知道接下来就会调用 acquireQueued(final Node node, int arg) 执行排队获取的操作,这个操作会伴随阻塞、唤醒、自旋等相关的动作,对应源码如下:

final boolean acquireQueued(final Node node, int arg) {
  boolean interrupted = false;
  try {
    // 自旋,要么获取锁,要么中断
    for (;;) {
      // 如果当前节点的前驱节点为头节点 说明当前节点是队列的最前面有效节点(头节点是虚节点) 就尝试获取锁
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        // 获取锁成功后 将当前节点设置为头节点(虚节点)
        setHead(node);
        p.next = null; // help GC
        return interrupted;
      }
      // 进行到这里说明p(前驱)为头节点且当前节点没有获取到锁(可能是非公平锁被抢占了)或者是p(前驱)不为头结点,
      // 这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源
      if (shouldParkAfterFailedAcquire(p, node))
        interrupted |= parkAndCheckInterrupt();
    }
  } catch (Throwable t) {
    // 
    cancelAcquire(node);
    if (interrupted)
      selfInterrupt();
    throw t;
  }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  int ws = pred.waitStatus;
  // 前驱waitStatus为SIGNAL 当前node需要阻塞等待
  if (ws == Node.SIGNAL)

    return true;
  // 其他状态不阻塞
  if (ws > 0) {
    // 前驱节点是取消状态 则跳过取消状态的节点(并将取消节点从队列中删除)
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    // 将前驱节点的等待状态设置为SIGNAL
    pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
  }
  return false;
}

我们一步一步来看,首先可以看出来如果当前节点的前驱为同步队列的 head 的话,那么当前节点对应的线程会再次尝试 tryAcquire,这也是我们上图 thread-2 刚刚入队之后会执行的流程,我们首先讨论 tryAcquire 失败后需要阻塞的流程(即 tryAcquire 时 thread-1 仍未unlock,aqs 的 exclusiveOwnerThread 仍为 thead-1),结合源码此时 aqs 对应的状态变迁如下所示:

万字长文分析 AQS 原理以及应用_第8张图片

结合我们的假设与上图,我们阐述一下 thread-2 执行 acquireQueued 的流程:

  • 首次循环,判断当前节点的前驱为 head 成功,tryAcquire(1) 一次但失败

  • tryAcquire(1) 失败后 shouldParkAfterFailedAcquire(p, node):node 的前驱 head 的waitStatus == 0 (初始化后的 Node 都为 0),将前驱 head 的 waitStatus 设为 1(SiGNAL- 代表其后继需要通知),并 return false(代表不需要阻塞)

  • 再次进入同一循环,判断当前节点的前驱为 head 成功,tryAcquire(1) 一次但失败

  • tryAcquire(1) 失败后 shouldParkAfterFailedAcquire(p, node):node 的前驱 head 的waitStatus == 1(SINGAL),代表当前节点需要阻塞等待通知,return true

  • 阻塞 thread-2

shouldParkAfterFailedAcquire(Node pred, Node node) 中还有一段代码用来处理前驱节点的waitStatus > 0(对应CANCELLED-1)时的情况,如果是这种情况说明前驱节点对应的线程 acquire 操作已经取消,此时把当前节点之前所有连续的 CANCELLED 节点从队列中删除即可(但要注意一点,删除 CANCELLED 只删除了 prev 指针,也即查询有效的是从后往前查询才能保证不会遇到已经删除的 CANCELLED 节点),这个也比较简单,不再多说。至此,AQS 独占式获取失败后入队、排队获取、阻塞的流程也完毕了,接下来就来看一下如果独占 AQS state的线程如果执行释放 state 之后的逻辑是怎样的。

4.2.2 AQS 之 release

前面我们已经分析了独占模式下 AQS acquire 的流程,涉及到的 case 包括获取成功、获取失败排队等待等,接下来就分析一下释放的流程,以 ReentrantReadWriteLock 的 WriteLock (独占模式),其解锁操作为:

public void unlock() {
  sync.release(1);
}

和 lock 一样,底层调用仍然是 aqs 的 API,写锁 unlock 时调用的 AQS 方法如下:

public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    // 头节点不为空并且头节点的waitStatus不是初始化节点状态,解除后继节点线程挂起状态
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h);
    return true;
  }
  return false;
}

release 整体流程如下:

万字长文分析 AQS 原理以及应用_第9张图片

对于 tryRelease(arg) 和 unparkSuccessor(h) 我们再展开看一下,首先看 tryRelease:

protected final boolean tryRelease(int releases) {
  if (!isHeldExclusively())
    throw new IllegalMonitorStateException();
  int nextc = getState() - releases;
  boolean free = exclusiveCount(nextc) == 0;
  if (free)
    setExclusiveOwnerThread(null);
  setState(nextc);
  return free;
}

这段代码简单,文字说明一下即可:

  • 首先检查一下是否是当前线程持有的 aqs(因为写锁是独占式的˙),如果不是抛出异常(不允许未加锁的线程解锁)

  • 允许 release: 将 state - releases (释放写锁量 因为不会涉及到高 16 位)

  • 如果 state 更新完其低 16 位为 0 (可重入的写锁已经全部释放) 将 AQS 的独占线程 exclusiveOwnerThread 设置为 null,return true

tryRelease 之后,如果头节点不为 null 并且 watiStatus != 0 (只要 head 后有节点,一般其waitStatus 都不为 0,前面 tryAcquire 时会自旋更改 waitStatus,可以结合前面加锁时 aqs 的状态图来看),就会开始唤醒其后继节点对应的线程,即:

private void unparkSuccessor(Node node) {
  // 获取头结点waitStatus 如果其<0(表示可能需要通知)清理其waitStatus为0,
  // 表示头节点后继节点不再需要通知了(因为这里完成之后排队等待的下线程抢占到state会成为新的head,通知状态也会转移到那里)
  int ws = node.waitStatus;
  if (ws < 0)
    node.compareAndSetWaitStatus(ws, 0);

  // 获取当前前节点的下一个节点
  Node s = node.next;
  // 以下的if代码段代表跳过取消的线程
  // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
  // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
  /*
   * 另外 找第一个非canceled节点时 是从后往前找 主要原因有 :
   * 1. addWaiter(Node node) 是先设置前驱节点 后设置后继节点 虽然这两步分别是原子的 但在两步之间还是可能存在后继节点未链接完成的情况
   * 2. 在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node
   * 如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点
   */
  if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node p = tail; p != node && p != null; p = p.prev)
      if (p.waitStatus <= 0)
        s = p;
  }
  // 通知head后非CANCELD状态节点(可能是head的后继 也可能是从tail向前遍历找到的第一个非CANCEL状态的节点)
  // 取消挂起状态,开始正常运行(结合之前的acquireQueued,线程被唤醒后会再次进入自旋的流程开始tryAcquire)
  if (s != null)
    LockSupport.unpark(s.thread);
}

一旦独占模式(写锁)被完全释放,那么head(虚节点)就开始唤醒后继节点(未 CANCEL )或者从 tail 往前遍历到的第一个非 CANCEL 的节点对应的线程。这时,被唤醒的线程对应的节点之前已经在排队阻塞了,一旦其被唤醒,又开始走之前我们分析的结合之前的 acquireQueued 内的自旋流程开始尝试获取 state,一旦获取到其就会成为 head (虚节点)并正常运行其线程,否则再次排队等待(这里就和我们之前分析的流程一摸一样了)。这里我们假设 thread-1 对应的写锁被释放,然后 thread-2 获取到写锁,则整个 thread-1 释放 -thread-2 获取的流程下 aqs 的状态如下图:

万字长文分析 AQS 原理以及应用_第10张图片

至此,ReentrantReadWriteLock 的 WriteLock 基于 AQS 实现加锁、释放锁的流程已经全部分析完了,涉及到了加锁成功、加锁失败排队阻塞、排队恢复之后继续抢占等逻辑,下一节我们开始分析 ReadLock 中用到的 AQS 的部分-共享模式下 aqs 的获取与释放。

4.3 AQS共享模式

上节我们分析了 AQS 的独占模式-已 WriteLock 为例,这节开始我们分析 AQS 的共享模式,对应 ReadLock 为 readLock.lock() 与 readLock.unlock(),AQS 中对应 acquireShared(1) 与 releaseShared(1)

4.3.1 AQS.acquireShared(int arg)

在读锁加锁时,调用 AQS.acquireShared(1),对应源码如下:

public final void acquireShared(int arg) {
  if (tryAcquireShared(arg) < 0)
    doAcquireShared(arg);
}

从上层来看和独占模式下的 acquire 是相似的,这里不再画图而是简单文字说明其整体流程:

  • 首先尝试共享模式获取 state,如果获取成功,方法结束

  • 获取失败,执行 doAcquireShared(1) 开始排队自旋执行共享获取直到成功退出或异常

4.3.1.1 Sync.tryAcquireShared(int unused)

当 ReadLock 以共享模式首次通过 Sync 尝试获取 state 时,源码逻辑如下:

protected final int tryAcquireShared(int unused) {
  Thread current = Thread.currentThread();
  int c = getState();
  // 已经加过写锁并且不是当前线程加的 不允许获取state 返回-1表示获取读锁失败
  if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
    return -1;
  // 读锁加锁次数
  int r = sharedCount(c);
  // 当不需要阻塞(公平锁判断同步队列是否有排队 非公平锁直接返回false)并且原子设置读锁加锁次数加1成功
  if (!readerShouldBlock() &&
      r < MAX_COUNT &&
      compareAndSetState(c, c + SHARED_UNIT)) {
    // 后面这一段记录一下每个线程加读锁的次数(暂时可以忽略)
    if (r == 0) {
      firstReader = current;
      firstReaderHoldCount = 1;
    } else if (firstReader == current) {
      firstReaderHoldCount++;
    } else {
      HoldCounter rh = cachedHoldCounter;
      if (rh == null ||
          rh.tid != LockSupport.getThreadId(current))
        cachedHoldCounter = rh = readHolds.get();
      else if (rh.count == 0)
        readHolds.set(rh);
      rh.count++;
    }
    // 返回获取到的state增量
    return 1;
  }
  return fullTryAcquireShared(current);
}

这段代码的主体逻辑也比较清晰(略去记录线程读锁次数):

  • 如果已经加过写锁 (state低 16 位不为 0)并且独占线程不等于当前线程,return -1 表示加读锁失败

  • 否则,如果不需要读阻塞(非公平锁不需要)并且加读锁次数不超过最大允许值,则将 state 高 16 位 +1(lock 中为 +1),记录一下线程对应的加读锁次数相关的数据,return 1 表示加读锁成功

首先,如果读锁首次加锁成功(必然没有写锁),那么此时 AQS 的状态也比较简单,因为 tryAcquireShared(1) 只更新了 state 的高 16 位,我们假设加读锁成功的 thread 为 thread-3,那么此时 AQS 的状态如下(省略 firstReader 等):

万字长文分析 AQS 原理以及应用_第11张图片

4.3.1.2 AQS.doAcquireShared(int arg)

当已经有其他线程加了写锁之后,那么当先线程加读锁就会失败(Sync.tryAcquireShared(1)失败),我们就已第4.2节结束时我们的AQS为例,此时aqs被thread-2独占,如下:

万字长文分析 AQS 原理以及应用_第12张图片

那么此后加读锁的逻辑就进入到 AQS.doAcquireShared(1),对应源码:

private void doAcquireShared(int arg) {
  final Node node = addWaiter(Node.SHARED);
  boolean interrupted = false;
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head) {
        int r = tryAcquireShared(arg);
        // 共享获取state成功
        if (r >= 0) {
          setHeadAndPropagate(node, r);
          p.next = null; // help GC
          return;
        }
      }
      // 共享获取state失败 判断是否需要阻塞
      if (shouldParkAfterFailedAcquire(p, node))
        interrupted |= parkAndCheckInterrupt();
    }
  } catch (Throwable t) {
    // 异常 取消获取操作(将当前节点从队列移除)
    cancelAcquire(node);
    throw t;
  } finally {
    if (interrupted)
      selfInterrupt();
  }
}

总体逻辑看上去和独占模式下的 acquireQueued(addWaiter(null), 1) 差不多,这里将入队与自旋全部放在了一个方法里,并且入队时 addWaiter 的入参也不再是 Node.EXCLUSIVE(null) 而是 Node.SHARED(new Node),还记得之前看的 addWaiter 方法吗?其逻辑为:

  • 新建一个 Node node 包装当前线程,node 的 nextWaiter 指向 mode( 即 addWaiter 传入的参数,独占模式下为 null,共享模式下为 new Node();),共享模式下 nextWaiter 指向的是一个非 null 的 Node,在读锁下其只是所为一个标识使用(结合 lock.condition() 相关 API 时其会有拓展,后面我们再说)

  • 如果 aqs 头节点为 null ,将 node 设置为 aqs 同步队列的 head 与 tail

  • 否则,将 node 追加到 aqs 的同步队列队尾,tail 指向 node

我们假设 doAcquireShared 首次自旋同样获取读锁失败 (thread-2 仍未释放写锁,tryAcquireShared(1) 失败),此时 shouldParkAfterFailedAcquire(p, node) 将头节点 waiterStatus 设置为 -1(SINGAL) 并不阻塞,至此在进入第二次自旋之前, aqs 的状态如下:

万字长文分析 AQS 原理以及应用_第13张图片

和独占式一样的,再次进入 for 循环我们同样假设 tryAcquireShared(1),那么此时 shouldParkAfterFailedAcquire(p, node) 就会返回 true (因为上一次循环中将 head 的 waitStatus 设置为了 -1),则此时将会挂起当前加读锁的线程,等待其被唤醒,此时 aqs 的状态和上图基本一致,只是 thread-3 的状态变为 waiting 状态(阻塞),如下:

万字长文分析 AQS 原理以及应用_第14张图片

此时,加读锁时由于读写锁互斥而导致加读锁的线程进入同步队列等待并阻塞的流程就完毕了。下一小节我们分析共享模式下涉及到的释放操作逻辑。

4.3.1 如何唤醒被阻塞的加读锁过程

这一小节叫“如何唤醒被阻塞的加读锁”而不叫 “AQS.releaseShared(1)” 是因为:对 ReentrantReadWriteLock 来说,其加读锁失败阻塞被唤醒可能写锁释放或者读锁获取后的传播行为,听起来比较拗口。我们对这两种情况逐一分析,假设 aqs 的状态如图所示:

万字长文分析 AQS 原理以及应用_第15张图片

此时, aqs state 被写锁 (thread-2) 独占,两个加读锁的线程在同步队列中等待 (thread-3 & thread-4) 被唤醒。

首先:第一个被唤醒的读锁线程肯定是写锁释放导致的(写锁释放之前读锁全部只能排队等待),我们假设此后 thread-2 释放写锁 (sync.release(1) 对应 writeLock.unlock()),结合我们之前的分析,独占模式下的释放会唤醒同步队列虚节点 (head) 的后继节点,那么此时的流程和之前一致,thread-3 被唤醒,我们假设其第 3 次自旋 tryAcquireShared(1) 获取到读锁,那么此时 aqs 的状态如下:

万字长文分析 AQS 原理以及应用_第16张图片

这时,加读锁的 thread-3 恢复后获取到读锁继续执行,这时就属于独占模式的释放唤醒同步队列中共享模式等待的线程。

另外一种情况,thread-4 此时仍处于阻塞状态,并且在这一事件点写锁已经被释放过了,那么此时共享模式的唤醒就要靠其传播特性,对应源码为:

for (;;) {
  final Node p = node.predecessor();
  if (p == head) {
    int r = tryAcquireShared(arg);
    // 获取state成功
    if (r >= 0) {
      setHeadAndPropagate(node, r);
      p.next = null; // help GC
      return;
    }
  }
  if (shouldParkAfterFailedAcquire(p, node))
    interrupted |= parkAndCheckInterrupt();
}

这里还是 doAcquireShared(int arg) 中的自旋代码,此时 threrad-3 tryAcquireShared(1) 已经成功,成功获取到返回 -state 增量 1,那么此时就会执行后续代码 setHeadAndPropagate(node, r)。这里就会涉及到共享模式下的释放传播特性,而且这一特性是共享模式通用的并不仅限于读写锁,同样也适用于实现了共享模式的 Semaphore、CountDownLatch( 因为 doAcquireShared(int arg) 中只有 tryAcquireShared(arg) 是 AQS 子类的实现,其余部分都是 AQS 已经实现的能力)。

setHeadAndPropagate(Node node, int propagate),见名知义我们可以猜测其作用是设置队列头并传播共享行为,我们就看一下相关的源码细节:

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head; // Record old head for check below
  setHead(node);
  if (propagate > 0 || h == null || h.waitStatus < 0 ||
      (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared())
      doReleaseShared();
  }
}

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

这里的逻辑可以这样理解:被唤醒的共享模式的节点(之前被阻塞)获取到 state 状态增量后,其自身变为头节点,并且在共享模式的获取增量转换为 propagate > 0 以及头节点的状态不为取消状态,则其唤醒行为需要向后继节点传播,保证共享模式下的共享不阻塞(最终的效果是只要共享 state 可以获取,则多个线程可以同时运行-即不互斥)。这里的描述可能有点绕,大家可以结合源码自己顺着这个思路自己去理一下。另外还有一点需要注意,共享模式的唤醒传播是通用的,无论是 ReentrantReadWriteLock 的读锁还是 Semaphore、CountDownLatch,目的就是保证共享模式下 state 可用时,排队等待的线程都有机会去获取 state (而不是像独占模式下一旦排队其唤醒是伴随停顿式的级联,这里是会一直级联传播,直到对应的线程被阻塞-如 Semaphore 的信号量被耗尽,CountDownLatch 的计数器又重新不为 0 ),对读锁来说其第一个阻塞的加读锁线程被唤醒之后,同步队列中所有等待的加读锁的线程都会被依次唤醒,此后各个读锁线程是并发的状态,直到所有并发的读锁都释放后又有新的写锁成功锁定,接下里就是不断重复这个过程罢了。重新回到读写锁,如果 thread-4 被 thread-3 对应的节点唤醒后也获取到了读锁,那么此时的 aqs 长这个样子(忽略记录线程相关的读锁加锁次数的变量): 

万字长文分析 AQS 原理以及应用_第17张图片

 此时 aqs 的 state 为 0x0002000(高 16 位为 2)代表被加了两次读锁,分别加了一次读锁的 thread-3 与 thread-4 并发的执行,aqs 的 exclusiveOwnweThread 为 null(表示其未被独占)。至此,主体脉络上:AQS 在 ReentrantReadWriteLock 中的应用与原理已经剖析完了,当多个线程不断的加读锁、写锁的过程中,就是按照上面的流程不断的获取成功/失败、排队阻塞/唤醒/传播,这样的过程不断重复,在独占模式与共享模式上都做了实现:AQS 的实现是通用的,而读写锁中 Syc 实现的 tryAcquire、tryRelease、tryAcquireShared、trcyReleaseShared 是根据其读写互斥、写写互斥、读读不互斥的需要实现的,大家可以理解一下这个思想,对理解 AQS 很有帮助。

5 、类比一下 - AQS 在 Semaphore 中的应用

前面几节我们花了大量的篇幅分析了 AQS 的原理以及其在 ReentrantReadWriteLock 中的使用,是因为读写锁同时包含了独占模式也包含了共享模式。如果前面的原理看懂了,那么理解 AQS 在 juc 下其他组件下的应用就会非常非常简单,我们就已信号灯 Semaphore 为例来验证一下:

Semaphore 字面意思是信号灯,在实际编程使用中,初始化 Semaphore 会给其一个总可用的信号量,如

public static void main(String[] args) {
  Semaphore semaphore = new Semaphore(2);
  for (int i = 0; i < 4; i++) {
    new Thread(() -> {
      try {
        semaphore.acquire();
        System.out.println(Thread.currentThread() + "获取信号量成功");
        TimeUnit.MILLISECONDS.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        System.out.println(Thread.currentThread() + "释放信号量");
        semaphore.release();
      }
    }).start();
  }
}

上面这段代码非常简单(为了打印顺序不错乱,我将"释放"打印在了真正释放之前),其输出结果为:

Thread[Thread-2,5,main]获取信号量成功
Thread[Thread-1,5,main]获取信号量成功
Thread[Thread-1,5,main]释放信号量
Thread[Thread-2,5,main]释放信号量
Thread[Thread-0,5,main]获取信号量成功
Thread[Thread-3,5,main]获取信号量成功
Thread[Thread-0,5,main]释放信号量
Thread[Thread-3,5,main]释放信号量

上面这段代码的含义为:初始化一个信号灯(默认为非公平的),其共享信号量为 2,在信号量可用(不为 0)时任何一个线程可以获取信号量,每次获取信号量都会-1 并返回信号灯剩余量(>=0 表示获取成功,<0 表示获取失败),当信号量耗尽(为 0 ),其余尝试获取信号量的线程都要阻塞(此时返回变更后的 remaining<0 ),并且任何一个线程 release 信号灯时其信号量+1 (空出来的信号量可以被其他线程获取)。

我们可以看一下 Semaphore 非公平模式下其 AQS 实现:

 public Semaphore(int permits) {
   // 默认使用非公平策略
   sync = new NonfairSync(permits);
 }

static final class NonfairSync extends Sync {

  NonfairSync(int permits) {
    super(permits);
  }

  protected int tryAcquireShared(int acquires) {
    // 调用Sync的非公平获取
    return nonfairTryAcquireShared(acquires);
  }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
  final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
      // 获取可用信号量
      int available = getState();
      // 将信号量 - 要获取的量值(一般是1)
      int remaining = available - acquires;
      // 如果
      if (remaining < 0 ||
          compareAndSetState(available, remaining))
        return remaining;
    }
  }
  
  protected final boolean tryReleaseShared(int releases) {
    for (;;) {
      // 获取可用信号量
      int current = getState();
      // 将信号量 + 要释放的量值(一般是1)
      int next = current + releases;
      if (next < current) // overflow
        throw new Error("Maximum permit count exceeded");
      if (compareAndSetState(current, next))
        return true;
    }
  }
}

上面这段代码基本都占据了 Semaphore 的大部分篇幅(可见 Semaphore 对 AQS 只做了薄薄一层扩展),整体逻辑也非常简单:

  • Semaphore 中 AQS 中的 state 表示可用信号量,并在初始化 Semaphore 时指定

  • 每次线程尝试获取信号量 (-1) 后如果信号量剩余量>=0 返回成功,在 AQS 中不阻塞,否则返回值<0 (表示获取失败),则相应线程在 AQS 中进入同步队列阻塞等待唤醒

  • 任何一个持有信号量的线程释放信号量时,其剩余信号量 +1,返回成功,然后在 AQS 通用逻辑中执行唤醒共享传播行为,后续排队线程被级联唤醒去获取信号量直到信号量再次被耗尽,然后循环此过程。

整体来看,如果理解了 AQS,那么 Semaphore 理解起来就非常简单,其只是简单的对其 state 作出定义与共享模式实现,其余逻辑定义都在 AQS 中通用,这里也不在画图说明,大家可以自己延伸一下。

6 、延伸一下 - AQS 和公平与非公平的关系

AQS 的原理基本介绍的差不多了,还有一些细枝的扩展没有涉及到,比如公平与非公平策略,这一节就来聊一聊 AQS 应用如何实现公平或者非公平策略。首先,在举例之前,我想首先抛出结论:非公平策略下所有的获取 state 操作不会判断同步队列中是否有排队线程,所有线程随机抢占(抢占不到就入队等待);而公平策略下所有获取操作在尝试获取 state 之前首先会判断同步队列中是否存在排队线程,如果存在排队线程则当前线程会放弃尝试获取 state,直接入队等待。

以ReentrantReadWriteLock 为例,其默认为非公平锁,其写锁调用 Sync 获取为:

protected final boolean tryAcquire(int acquires) {
           
  Thread current = Thread.currentThread();
  int c = getState();
  int w = exclusiveCount(c);
  if (c != 0) {
    // (Note: if c != 0 and w == 0 then shared count != 0)
    if (w == 0 || current != getExclusiveOwnerThread())
      return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
      throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
  }
  // 这里会判断是否需要阻塞
  if (writerShouldBlock() ||
      !compareAndSetState(c, c + acquires))
    return false;
  setExclusiveOwnerThread(current);
  return true;
}

注意一下这段逻辑:

if (writerShouldBlock() ||
      !compareAndSetState(c, c + acquires))
    return false;

而其在读写锁中非公平策略下实现为 NonfairSync:

 static final class NonfairSync extends Sync {
   private static final long serialVersionUID = -8159625535654395037L;
   final boolean writerShouldBlock() {
     return false; // writers can always barge
   }
 }

而公平策略对应实现为 FairSync:

static final class FairSync extends Sync {
  private static final long serialVersionUID = -2274990926593161451L;
  final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
  }
}

可以看出来,非公平策略下 writerShouldBlock() 直接返回 false,说明不需要判断同步队列是否有排队,而公平策略下返回 hasQueuedPredecessors() 的结果(即判断同步队列中是否有排队节点),如果有则返回 false 表示获取失败,后续就会走入队排队的逻辑从而实现公平策略。

而 hasQueuedPredecessors() 是 AQS 的实现,其目的非常简单,就是判断同步队列是否存在排队节点:

public final boolean hasQueuedPredecessors() {
  Node h, s;
  if ((h = head) != null) {
    // 跳过取消节点
    if ((s = h.next) == null || s.waitStatus > 0) {
      s = null; // traverse in case of concurrent cancellation
      for (Node p = tail; p != h && p != null; p = p.prev) {
        if (p.waitStatus <= 0)
          s = p;
      }
    }
    if (s != null && s.thread != Thread.currentThread())
      return true;
  }
  return false;
}

其他 AQS 的应用譬如 ReentrantLock、Semaphore 实现公平与非公平的实现基本和这里完全一致,大家可以自己看一下。

7 、总结

至此,本文的内容就结束了,全文花了大量篇幅阐述 AQS 的数据结构、算法、API 实现以及其在 ReentrantReadWriteLock 中对于独占模式、共享模式的实现与复用,还类比了一点Semaphore 的内容来验证我们阐述的内容,最后顺便提了一下公平策略和非公平策略的实现。全文又臭又长,希望可以帮助大家理解 AQS 的原理,源码细节很多,大家可以借鉴本文的思路自己结合源码理解。另外,由于篇幅原因,本文也遗留了一些内容:如 Lock 的 Condition 如何基于 AQS 的 nextWaiter 实现,有机会的话会把这部分内容补上,当然如果彻底理解了 AQS 原理,那理解 Condition 运用 AQS 的等待队列实现条件等待的原理也会非常简单。

另外,Eureka 源码跟读第二篇等下次吧(上次搞了一次 redisson 分布式锁提到了 aqs 就想先把 AQS 补上),手动狗头!

8 、参考内容

JDK11 源码

全文完


以下文章您可能也会感兴趣:

  • 简单说说spring的循环依赖

  • Mysql redo log 漫游

  • 单元测试的实践之路

  • 可线性化检查:与 NP 完全问题做斗争

  • Java 类型系统从入门到放弃

  • Webpack 快速上手(下)

  • Webpack 快速上手(中)

  • Webpack 快速上手(上)

  • Airbnb 的 React Native 之路(下)

  • Airbnb 的 React Native 之路(上)

  • 零基础玩转 Serverless

  • iOS 开发:深入理解 Xcode 工程结构(一)

  • 三大报表:财务界的通用语言

  • 四维阅读法 - 我的高效学习“秘技”

  • 一个创业公司的容器化之路(三) - 容器即未来

  • 一个创业公司的容器化之路(二) - 容器化

  • 一个创业公司的容器化之路(一) - 容器化之前

  • 乐高式微服务化改造(下)

  • 乐高式微服务化改造(上)

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected]

万字长文分析 AQS 原理以及应用_第18张图片

你可能感兴趣的:(队列,java,编程语言,uefi,subversion)