本文首发于一世流云的专栏: https://segmentfault.com/blog...
AbstractQueuedSynchronizer抽象类(如下简称AQS)是整个java.util.concurrent
包的核心。在JDK1.5时,Doug Lea引入了J.U.C包,该包中的大多数同步器都是基于AQS来构建的。AQS框架提供了一套通用的机制来管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。java
咱们所熟知的ReentrantLock、CountDownLatch、CyclicBarrier等同步器,其实都是经过内部类实现了AQS框架暴露的API,以此实现各种同步器功能。这些同步器的主要区别其实就是对同步状态(synchronization state)的定义不一样。node
AQS框架,分离了构建同步器时的一系列关注点,它的全部操做都围绕着资源——同步状态(synchronization state)来展开,并替用户解决了以下问题:segmentfault
这实际上是一种典型的模板方法设计模式:父类(AQS框架)定义好骨架和内部操做细节,具体规则由子类去实现。
AQS框架将剩下的一个问题留给用户:
什么是资源?如何定义资源是否能够被访问?设计模式
咱们来看下几个常见的同步器对这一问题的定义:安全
同步器 | 资源的定义 |
---|---|
ReentrantLock | 资源表示独占锁。State为0表示锁可用;为1表示被占用;为N表示重入的次数 |
CountDownLatch | 资源表示倒数计数器。State为0表示计数器归零,全部线程均可以访问资源;为N表示计数器未归零,全部线程都须要阻塞。 |
Semaphore | 资源表示信号量或者令牌。State≤0表示没有令牌可用,全部线程都须要阻塞;大于0表示由令牌可用,线程每获取一个令牌,State减1,线程没释放一个令牌,State加1。 |
ReentrantReadWriteLock | 资源表示共享的读锁和独占的写锁。state逻辑上被分红两个16位的unsigned short,分别记录读锁被多少线程使用和写锁被重入的次数。 |
综上所述,AQS框架提供了如下功能:并发
因为并发的存在,须要考虑的状况很是多,所以可否以一种相对简单的方法来完成这两个目标就很是重要,由于对于用户(AQS框架的使用者来讲),不少时候并不关心内部复杂的细节。而AQS其实就是利用模板方法模式来实现这一点,AQS中大多数方法都是final或是private的,也就是说Doug Lea并不但愿用户直接使用这些方法,而是只覆写部分模板规定的方法。
AQS经过暴露如下API来让让用户本身解决上面提到的“如何定义资源是否能够被访问”的问题:框架
钩子方法 | 描述 |
---|---|
tryAcquire | 排它获取(资源数) |
tryRelease | 排它释放(资源数) |
tryAcquireShared | 共享获取(资源数) |
tryReleaseShared | 共享获取(资源数) |
isHeldExclusively | 是否排它状态 |
还记得Lock接口中的那些锁中断、限时等待、锁尝试的方法吗?这些方法的实现其实AQS都内置提供了。
使用了AQS框架的同步器,都支持下面的操做:工具
Condition接口,能够看作是Obejct类的wait()、notify()、notifyAll()方法的替代品,与Lock配合使用。
AQS框架内部经过一个内部类ConditionObject
,实现了Condition接口,以此来为子类提供条件等待的功能。ui
在本章第一部分讲到,AQS利用了模板方法模式,其中大多数方法都是final或是private的,咱们把这类方法称为Skeleton Method,也就是说这些方法是AQS框架自身定义好的骨架,子类是不能覆写的。
下面会按类别简述一些比较重要的方法,具体实现细节及原理会在本系列后续部分详细阐述。this
CAS,即CompareAndSet,在Java中CAS操做的实现都委托给一个名为UnSafe类,关于Unsafe
类,之后会专门详细介绍该类,目前只要知道,经过该类能够实现对字段的原子操做。
方法名 | 修饰符 | 描述 |
---|---|---|
compareAndSetState | protected final | CAS修改同步状态值 |
compareAndSetHead | private final | CAS修改等待队列的头指针 |
compareAndSetTail | private final | CAS修改等待队列的尾指针 |
compareAndSetWaitStatus | private static final | CAS修改结点的等待状态 |
compareAndSetNext | private static final | CAS修改结点的next指针 |
方法名 | 修饰符 | 描述 |
---|---|---|
enq | private | 入队操做 |
addWaiter | private | 入队操做 |
setHead | private | 设置头结点 |
unparkSuccessor | private | 唤醒后继结点 |
doReleaseShared | private | 释放共享结点 |
setHeadAndPropagate | private | 设置头结点并传播唤醒 |
方法名 | 修饰符 | 描述 |
---|---|---|
cancelAcquire | private | 取消获取资源 |
shouldParkAfterFailedAcquire | private static | 判断是否阻塞当前调用线程 |
acquireQueued | final | 尝试获取资源,获取失败尝试阻塞线程 |
doAcquireInterruptibly | private | 独占地获取资源(响应中断) |
doAcquireNanos | private | 独占地获取资源(限时等待) |
doAcquireShared | private | 共享地获取资源 |
doAcquireSharedInterruptibly | private | 共享地获取资源(响应中断) |
doAcquireSharedNanos | private | 共享地获取资源(限时等待) |
方法名 | 修饰符 | 描述 |
---|---|---|
acquire | public final | 独占地获取资源 |
acquireInterruptibly | public final | 独占地获取资源(响应中断) |
acquireInterruptibly | public final | 独占地获取资源(限时等待) |
acquireShared | public final | 共享地获取资源 |
acquireSharedInterruptibly | public final | 共享地获取资源(响应中断) |
tryAcquireSharedNanos | public final | 共享地获取资源(限时等待) |
方法名 | 修饰符 | 描述 |
---|---|---|
release | public final | 释放独占资源 |
releaseShared | public final | 释放共享资源 |
咱们在第一节中讲到,AQS框架分离了构建同步器时的一系列关注点,它的全部操做都围绕着资源——同步状态(synchronization state)来展开所以,围绕着资源,衍生出三个基本问题:
同步状态的定义
同步状态,其实就是资源。AQS使用单个int(32位)来保存同步状态,并暴露出getState、setState以及compareAndSetState操做来读取和更新这个状态。
/** * 同步状态. */ private volatile int state; protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } /** * 以原子的方式更新同步状态. * 利用Unsafe类实现 */ protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
在JDK1.5以前,除了内置的监视器机制外,没有其它方法能够安全且便捷得阻塞和唤醒当前线程。
JDK1.5之后,java.util.concurrent.locks包提供了LockSupport类来做为线程阻塞和唤醒的工具。
等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。
等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,所以也叫CLH队列。
1. 结点定义
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。
每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
结点状态 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,须要移出队列 |
SIGNAL | -1 | 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改成SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) |
CONDITION | -2 | Condition专用。表示当前结点在Condition队列中,由于等待某个条件而被阻塞了 |
PROPAGATE | -3 | 传播。适用于共享模式(好比连续的读操做结点能够依次进入临界区,设为PROPAGATE有助于实现这种迭代操做。) |
INITIAL | 0 | 默认。新结点会处于这种状态 |
AQS使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,之因此这种作是由于在双向链表的结构下,这样更容易实现取消和超时功能。
next指针:用于维护队列顺序,当临界区的资源被释放时,头结点经过next指针找到队首结点。
prev指针:用于在结点(线程)被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动做。
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; /** * INITAL: 0 - 默认,新结点会处于这种状态。 * CANCELLED: 1 - 取消,表示后续结点被中断或超时,须要移出队列; * SIGNAL: -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改成SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) * CONDITION: -2- Condition专用,表示当前结点在Condition队列中,由于等待某个条件而被阻塞了; * PROPAGATE: -3- 传播,适用于共享模式。(好比连续的读操做结点能够依次进入临界区,设为PROPAGATE有助于实现这种迭代操做。) * * waitStatus表示的是后续结点状态,这是由于AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。 */ volatile int waitStatus; // 前驱指针 volatile Node prev; // 后驱指针 volatile Node next; // 结点所包装的线程 volatile Thread thread; // Condition队列使用,存储condition队列中的后继节点 Node nextWaiter; Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } }
2. 队列定义
对于CLH队列,当线程请求资源时,若是请求不到,会将线程包装成结点,将其挂载在队列尾部。
CLH队列的示意图以下:
①初始状态,队列head和tail都指向空
②首个线程入队,先建立一个空的头结点,而后以自旋的方式不断尝试插入一个包含当前线程的新结点
/** * 以自旋的方式不断尝试插入结点至队列尾部 * * @return 当前结点的前驱结点 */ private Node enq(final Node node) { for (; ; ) { Node t = tail; if (t == null) { // 若是队列为空,则建立一个空的head结点 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
本章简要介绍了AQS的思想和原理,读者能够参考Doug Lea的论文,进一步了解AQS。直接阅读AQS的源码比较漫无目的,后续章节,将从ReentrantLock、CountDownLatch的使用入手,讲解AQS的独占功能和共享功能。