Java同步器AbstractQueuedSynchronizer简称AQS(文中全称和简写混用),在java.util.concurrent包中很多依赖状态的API都是基于AQS实现的,比如常用的:ReentrantLock、Semaphore、CountDownLatch、ThreadPoolExecutor等等。
可以说AQS是java并发包实现的基石,深入理解AQS可以帮助我们更好的是理解java并发api,而不仅仅停留在使用上。同时我们也可以基于AQS实现一些自定义的可阻塞类,虽然大部分时候不需要我们这样做,因为使用现有的api基本已经足够了。
AbstractQueuedSynchronizer直译过来是“抽象的同步队列”,也就是说AQS本质上是维护一个队列,至于你想用这个队列来做什么AQS不管,具体由子类来定。既然AQS是抽象类,就必须要有子类去实现,但在java.util.concurrent包中没有直接直接实现AQS的子类api,其子类都是作为私有的内部类,在各个API使用。下图为jdk1.8中对AQS实现的子类(在jdk1.6中更多):
可以看到一共有11个子类,并且都内部类。AQS在java.util.concurrent并发包中,主要有以下4个功能:
1、实现锁:ReentrantLock(重入锁)、ReentrantReadWriteLock(重入读写锁),并同时提供公平锁和非公平锁实现。
2、实现信号量:Semaphore(主要用于控制某个资源,可以被同时访问的线程个数),并同时支持公平和非公平实现。
3、实现闭锁:CountDownLatch,一种同步辅助工具,可以实现:在一组其他线程中执行的操作完成之前,阻塞一个或多个线程;并在完成之后,同时唤起被阻塞的一个或者多个线程。
4、实现线程池执行器:ThreadPoolExecutor,这时java线程池框架的基石。
AQS的基本构成
节点定义类Node
前面提到AQS本质上是维护一个队列,首先来看下这个队列中的成员:Node,它是AQS定义的内部类,主要成员变量信息如下:
- static final class Node {
- volatile int waitStatus;//节点状态
- volatile Node prev;//前指针 指向前一个节点
- volatile Node next;//后指针指向后一个节点
- volatile Thread thread;//线程实例
- Node nextWaiter;//下一个等待节点
- /**状态列表,对应waitStatus字段值*/
- //共享类型
- static final Node SHARED = new Node();
- //独占类型
- static final Node EXCLUSIVE = null;
- //线程取消类型
- static final int CANCELLED = 1;
- //线程唤醒状态类型 对应condition的 signal、signalAll方法
- static final int SIGNAL = -1;
- //线程阻塞状态类型,对应condition的await方法
- static final int CONDITION = -2;
- //对应共享类型释放资源时,传播唤醒线程状态
- static final int PROPAGATE = -3;
- //省略其他
- }
可以看到每个节点中都有一个“线程实例”,以及该线程所处的状态waitStatus,以及用于表示前后指针的成员prev、nex,即AQS是双向链表,节点里的“线程实例”其实就是阻塞的线程列表。AQS的主要方法其实就是操作和维护这个双向链表。
成员变量
在来看下AQS的主要成员变量:
- //队列的头节点
- private transient volatile Node head;
- //队列的尾节点
- private transient volatile Node tail;
- //资源 状态
- private volatile int state;
其中最重要的就是队列状态state(注意跟Node中的节点状态区分开),这个字段一般由于表示一种共享的“资源”的状态,比如:在ReentrantLock锁的实现中,它表示锁是否被占用(为0是表示可用);在信号量Semaphore的实现中,表示剩余的“许可数量”,大于0表示可用;CountDownLatch中表示需全部完成的工作数量。
AQS的核心功能就是:在多线程竞争有限的“资源”的情况下,只允许部分线程(或单个线程)访问这种“资源”,并阻塞其他“线程”。AQS的所有方法几乎都是在根据“资源”的状态,操作和维护一个“双向链表”。
主要方法
前面提到过,由于资源是有限的,所以AQS的核心方法分为两大类:获取“资源”方法和释放“资源”方法,同时这两类方法又都有公平和非公平实现。另外对应 获取“资源”方法,还有延时获取和可中断获取。由于篇幅有限这里不贴出所有的方法,只分类讲解入口方法的主要流程,如果要深入每个方法,可以通过这些入口方法断点跟下去即可。下面分别来看:
独占获取和释放方法
独占获取方法acquire,对应的释放方法release。独占的方式获取“资源”,那这个“资源”状态state其实就只有两种:可以、非可用,一般用于实现独占锁。首先来看acquire方法:
- public final void acquire(int arg) {
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
简易流程如下:
通过调用tryAcquire(arg)方法尝试获取“资源”,如果获取到 该线程继续执行;否则调用acquireQueued方法加入队列,并阻塞该线程,注意节点是Node.EXCLUSIVE独占方式。其中tryAcquire(arg)方法是交给子类去实现的,在jdk的api中独占方式一般都是用于获取“锁”,具体的实现可以看下ReentrantLock、ReentrantReadWriteLock中的写锁、ThreadPoolExecutor。
再来看下释放资源方法release:
- public final boolean release(int arg) {
- if (tryRelease(arg)) {
- Node h = head;
- if (h != null && h.waitStatus != 0)
- unparkSuccessor(h);
- return true;
- }
- return false;
- }
简易流程如下:
通过调用tryRelease(arg)方法尝试释放资源,其实就是修改“资源”状态state;如果资源释放成功就调用unparkSuccessor方法,唤醒队列头结点的线程(具体是在上述acquireQueued方法中进行阻塞的),继续执行。同样 tryRelease(arg)方法是在子类中实现。
共享获取和释放方法
共享获取方法acquireShared,对应的释放方法为releaseShared。在java的api中主要用于实现:ReentrantReadWriteLock中的读锁(共享锁)、Semaphore。首先看下共享获取方法acquireShared:
- public final void acquireShared(int arg) {
- if (tryAcquireShared(arg) < 0)
- doAcquireShared(arg);
- }
通过tryAcquireShared(arg)方法获取“资源”,如果获取到“资源”直接返回,该线程继续执行。如果没有获取到资源,就通过调用doAcquireShared加入队列,并阻塞该线程。这里tryAcquireShared(arg)方法是有子类实现的,具体实现可以参考ReentrantReadWriteLock中的读锁、Semaphore。ReentrantReadWriteLock的获取读锁实现要稍微复杂些,需要判断是否被读锁占有;Semaphore的实现比较简单,就是判断剩余“许可数量”是否大于0。再来看下释放方法releaseShared:
- public final boolean releaseShared(int arg) {
- if (tryReleaseShared(arg)) {
- doReleaseShared();
- return true;
- }
- return false;
- }
通过tryReleaseShared(arg)方法尝试释放资源,如果释放成功,调用doReleaseShared方法唤醒在上述acquireShared 方法中阻塞的线程。同样的tryReleaseShared(arg)方法是在子类中实现的。
共享获取、排它独占获取,二者几乎没有区别,只是在释放方法中有区别,独占方式释放资源后,只会唤醒“头结点”的线程;而共享方式,会遍历队列,把满足条件的线程全部唤醒,具体可以参考doReleaseShared和acquireQueued方法。
可中断获取方法
可中断获取方法acquireInterruptibly(独占可中断获取)、acquireSharedInterruptibly(共享可中断获取),这两个方法中的独占和共享的实现与上述讲述过程相同,不再累述。如果获取不成功就进入排队并阻塞当前线程,唯一的区别就是会抛出InterruptedException异常,也就是说可以在其他线程调用该线程的interrupt方法,中断该线程放弃任务执行,从而放弃排队。以acquireInterruptibly方法实现为例,如下:
- public final void acquireInterruptibly(int arg)
- throws InterruptedException {
- if (Thread.interrupted())
- throw new InterruptedException();
- if (!tryAcquire(arg))
- doAcquireInterruptibly(arg);
- }
与独占获取方法一样 通过tryAcquire(arg)方法获取资源,如果没有获取到就调用doAcquireInterruptibly(arg)方法把当前线程加入队列,并阻塞当前线程。
延迟获取方法
延迟获取方法tryAcquireNanos(独占延迟获取)、tryAcquireSharedNanos(共享延迟获取),这两个方法中的独占和共享的实现与上述讲述过程相同,不再累述。如果获取不成功就进入排队并阻塞当前线程,唯一的区别就是这里阻塞会有时间限制,如果超时就抛出InterruptedException异常,中断该线程放弃任务执行,从而放弃排队。
注意与“可中断获取方法”的区别:前者是时间到了,自动中断线程放弃排队;后者是需要外部手动触发。这里以tryAcquireNanos(独占延迟获取)为例,代码实现如下:
- public final boolean tryAcquireNanos(int arg, long nanosTimeout)
- throws InterruptedException {
- if (Thread.interrupted())
- throw new InterruptedException();
- return tryAcquire(arg) ||
- doAcquireNanos(arg, nanosTimeout);
- }
与独占获取方法一样 通过tryAcquire(arg)方法获取资源,如果没有获取到就调用doAcquireNanos方法把该线程加入队列,并阻塞该线程,与acquireQueued方法不同地方就是阻塞会有一个时间限制,当阻塞时间到达时抛出InterruptedException异常。
可以看到AQS的核心方法分为三类:入口方法,tryXXX方法,doXXX方法。
入口方法:前面已经列出;
tryXXX方法种:有4个方法是交给子类实现的:tryAcquire(尝试独占获取资源)、tryRelease(尝试独占释放资源)、tryAcquireShared(尝试共享获取资源)、tryReleaseShared(尝试释放共享资源)。
doXXX方法:是具体的实现加入队列,以及阻塞线程的核心实现,由于这些方法的代码比较长,就不贴出来了,可以根据上述思路自行查阅jdk API源码。
公平和非公平
所谓公平,就是所有的线程来获取“资源”时,都得按照FIFO的原则排队;所谓非公平,也不是说不排队,而是先检查“资源”是否可用,如果可用就插队立即使用,否则再进行排队。另外公平和非公平实现,都是在子类中实现的,在AQS中没有实现。比如ReentrantLock公平锁和非公平锁实现。
内部类 ConditionObject
最后提下AQS中的内部类:“条件队列”实现类ConditionObject,这个类是Condition接口的实现类(主要方法:await系列方法、signal、signalAll,对应Object类的wait、notify、notifyAll方法),主要用于结合Lock锁结合使用(通过Lock的newCondition()方法得到)。每一个Lock对应一个Condition条件队列,这个条件队列使用的是AQS中的同一个队列,只是队列Node节点中的waitStatus类型不同。比如ReentrantLock锁,通过lock方法可以向AQS中加入节点,也可以通过Condition的await方法向AQS加入节点,只是节点的类型不同而已。
由于条件队列Condition和Lock锁密切相关,关于这部分内容后面有时间再单独总结,这里不再继续展开。
总结
简单的总结AQS,本质上就是维护了一个“双向链表”结构的队列,其中每个节点保存了一个阻塞的线程;其主要作用就是用于多线程竞争有限的资源时,对线程阻塞、排队。另外我们也可以根据AQS实现自己的同步器,当然大部分时候使用现有的API实现类已经足够了。
http://moon-walker.iteye.com/blog/2406446