欢迎移步google drive 阅读原文:
https://drive.google.com/file/d/0B758ryeeYYQYa1k2cHhzT0ZOSlU/edit?usp=sharing
AQS全称AbstractQueuedSynchronizer,它是concurrent包中最重要的基础设施类之一,负责作为模板类向业务层提供对临界区的管理。本文以FutureTask的实现机制作为引子,介绍了AQS的业务背景和设计思路,最后梳理了AQS的代码实现。
摘要............................................................................................................................... 1
动机............................................................................................................................... 2
interrupt的真相.............................................................................................................................................................. 2
FUTURE TASK问题......................................................................................................................................................... 2
内部类Sync........................................................................................................................................................................... 4
层次结构....................................................................................................................... 4
业务层...................................................................................................................................................................................... 4
复用层...................................................................................................................................................................................... 7
代码分析....................................................................................................................... 8
如何复用AQS...................................................................................................................................................................... 8
【一个例子】.............................................................................................................................................................. 11
AQS内部细节................................................................................................................................................................... 15
概述.................................................................................................................................................................................... 15
线程控制......................................................................................................................................................................... 15
队列.................................................................................................................................................................................... 15
节点.................................................................................................................................................................................... 15
排他获取......................................................................................................................................................................... 17
排他释放......................................................................................................................................................................... 21
共享获取&释放.......................................................................................................................................................... 24
回到Future Task.......................................................................................................................................................... 29
interrupt的真相
一提起interrupt,大家往往想到的是:
1、其中文直译为“中断”
2、多线程编程中无处不在的InterruptedException
或许是受到了中文翻译的误导,我们不少人会以为调用Thread.interrupt()能中断任意线程。
其实,它只是表示有人要求中断,具体的实现是更新了一个boolean型的标志位(interrupt status)。至于是否响应中断以及何时中断,都是线程自己的事了。
这就好比老师在台上讲课,下面有学生想要提问,必须先举手(请求中断)。如果老师在写板书没看到(尚未轮询到中断标志位),则什么也不会发生;如果面对学生,那么老师一般会等到一句话说完,再允许该生发言(延迟响应中断)或者宣布“讲完这一节再统一提问”(忽略并重置中断)。
所以,调用Thread.interrupt()只是“举手”而已。
有关interrupt和InterruptedException的细节,请参考此文:http://blog.csdn.net/axman/article/details/431796
正如文中所说,“只有当线程执行到sleep,wait,join等方法时,或者自己检查中断标志而抛出异常的情况下,线程才会抛出InterruptedException。”
这样做的目的是允许线程安全可控地结束,避免盲目结束出现中间状态、资源没被释放等情况。
接下来转到FutureTask。这是concurrent包里提供一个很方便的工具类,用于接受指定线程作为宿主,异步运行一个任务。其get()方法能阻塞当前用于查询结果的线程(以下简称查询线程)直到异步线程运行结束。
之前一度以为,若调用FutureTask.cancel()方法,能随时中止异步任务。
事实并非如此。就像中断机制一样,若异步任务本身没有通过Thread.isInterrupted()主动检查中断标志,那么它不仅不会结束,也不会抛出中断异常。倒是多个查询线程都能即时抛出CancellationException。
观察FutureTask.cancel()方法,其代码调用了内部类Sync.innnerCancel(),后者看起来只是简单地触发了Thread.interrupt(),这符合我们的预期。但又是怎样的机制分别唤醒了处于阻塞状态的查询线程呢?代码中releaseShared(0)引起了我的注意。
boolean innerCancel(boolean mayInterruptIfRunning) {
for(;;) {
int s= getState();
if(ranOrCancelled(s))
return false;
if(compareAndSetState(s,CANCELLED))
break;
}
if(mayInterruptIfRunning) {
Thread r = runner;
if(r != null)
r.interrupt();
}
releaseShared(0);
done();
return true;
}
为了搞清楚这个问题的来龙去脉,我展开了对FutureTask内部类Sync的调查,于是自然延伸到了其基类AbstractQueuedSynchronizer(以下简称AQS)。AQS是如此庞杂,绝非三言两语能说得清楚;更糟糕的是,网上大量有关该类的文章和学习笔记,要么蜻蜓点水一笔带过,要么一上来就跳入了代码细节的深渊,为此我绕了一个大圈才理顺。当时多么希望有一篇文章能深入浅出,用先宏观再微观的梳理方式,把这个类讲讲清楚。现在我也来写一篇AQS的学习笔记,以飨小伙伴们。
先考虑如下现实世界中的情况:
(图片引用自http://www.jq1997.cn/mkldfffiles/2012531105117204.jpg)
l 例如,男厕是临界区,有些高端大气的小便槽(上图)可供一起使用(共享访问),容纳人数有限(资源有限);保洁员“可以”在入口处放一块牌子“清洁中”(排他访问),也可以同时清洁。
l 又如,游泳池是临界区,泳客是共享访问,一场结束的时候倒消毒液的人“必须”先清场,再操作(排他访问)
l 再如,停车场是临界区,车辆均共享访问,车位(资源)有限,且小车占一个车位,大巴占两个车位。
可见,上述业务经常变化的部分是对临界区的资源管理,主要包含:
l 临界区内不同的“资源总数”;
l 每次访问占用的“资源数量”;
l 支持两种不同的“访问方式”(排他/共享)。
至于临界区被占满后,如何排队、能否插队(此处特指跳过排队直接进入临界区,注1)等管理细节都是雷同的。
接下来回到Java的世界:
让我们先来看看concurrent包里常见的并发控制相关的业务类。
类名 | 功能 | 资源总数 | 如何访问 | 备注 |
Mutex | 互斥锁 | 1 | 仅排他 | 出现在AQS类的注释中 |
Semaphore | 信号量 | 可指定(构造参数permits) | 仅共享 |
|
ReentrantReadWriteLock | 读写(可重入)锁 | 无上限(实际为16位unsigned short) | 读锁共享访问,写锁排他访问 | 关于“可重入”见注2; |
注意到,表内与前述资源管理过程中抽象出来的“变数”是一致的。
因此,业务层的重点是掌控资源管理中易变的部分,把不变的部分交给复用层。这就好比为了细分市场,我们可以出多款不同外观、大小的汽车,但引擎、变速箱之类的动力系统只要经过简单的调校就可以复用了。
关于锁的术语有很多,小注如下:
注1 此处涉及锁的公平性。锁被释放后,排队中的线程只有唤醒后才能争夺锁,可能竞争不过新到来的活动线程,意即不公平;极端情况下甚至会造成排队的线程始终拿不到锁,即饿死锁。
注2 “可重入”意为允许已经获得锁的对象再次获得这把锁,只要标记信号量++,可避免上述场景下死锁。
注3 偏向锁、自旋锁。参考http://kenwublog.com/theory-of-java-biased-locking
下面简述业务层与复用层之间的依赖关系。
职责简述:
类名 | 角色 | 职责 |
Semaphore | 业务类 | 通过把业务方法代理给内部类Sync,提供同步工具的语义。 |
Sync | 代理类 | 通过继承AQS,并按需覆写钩子方法,来指定业务“变数”(对代理和钩子不明白的请参考代理模式和模板模式) |
AbstractQueuedSynchronizer | 模板类 | 向业务类提供模板方法,封装了如何以共享或排他的形式访问临界区,处理包括临界区满后如何排队等细节。(Queued暗示排队相关) |
AbstractOwnableSynchronizer |
| 记录排他访问的线程信息。(Ownable暗示排他相关) |
Step1/3. 定义state。
state是一个int,它没有具体语义,但作为AQS的核心成员变量,可根据业务类的需要赋予state具体的语义。
例如,对于信号量Semaphore,它可以表示当前临界区内可用资源的数量。这种场景下,一般会把它初始化成资源总量。占用时--,释放时++。为0时表示资源被占满。
又如,对于可重入锁ReentrantLock,初始化为0表示锁可用;为1表示被占用,为n表示重入的次数。
再如,对于ReentrantReadWriteLock,state逻辑上被分成两个16位的unsigned short,分别记录读锁被多少线程共享和写锁被重入的次数。
特别地,对于异步任务FutureTask,state承载着任务的运行状态:0代表ready;1代表running;2代表ran;4代表cancelled。
为了保证线程安全,读写state必须通过下述三个protected final方法。
方法 | 简介 |
getState() | 读状态 |
setState (int newState) | 写状态 |
compareAndSetState (int expect, int update) | CAS写状态 |
注:AQS的另一个版本AbstractQueuedLongSynchronizer,采用了64位的long作为state的数据类型。
Step2/3. 新建内部类Sync,继承自AQS并按需实现钩子方法。
钩子方法 | 简介 |
tryAcquire (int arg) | 排他获取(资源数) |
tryRelease(int arg) | 排他释放(资源数) |
tryAcquireShared(int arg) | 共享获取(资源数) |
tryReleaseShared(int arg) | 共享释放(资源数) |
isHeldExclusively() | 是否排他状态 |
获取和释放操作本质即变更state。
此处强调一下“按需”,如果你的业务只关心排他获取资源,比如Mutex这样的,就无需覆写共享相关的那一组方法了。这也是为何AQS身为一个抽象类,没有把钩子方法设为抽象方法,而是统一带默认实现(直接抛出UnsupportedOperationException)的原因。
Step3/3. 业务类把业务方法的实现委托给Sync。Sync可以直接使用继承自AQS的模板方法。
模板方法 | 简介 |
acquire(int arg) | 排他获取(获取过程不可中断) |
acquireInterruptibly(int arg) | 排他获取(获取过程可中断) |
tryAcquireNanos(int arg, long timeout) | 排他获取(超时结束) |
|
|
acquireShared(int arg) | 共享获取(一组同上) |
acquireSharedInterruptibly(int arg) |
|
tryAcquireSharedNanos(int arg, long timeout) |
|
|
|
release(int arg) | 排他释放 |
releaseShared(int arg) | 共享释放 |
|
|
hasQueuedThreads() | 是否排队 |
getQueueLength () | 排队长度 |
… | (忽略若干次要方法) |
下面以业务最为简单的“不可重入互斥锁”Mutex举例说明(出现在AQS类的注释中)。请阅读下述代码并回顾上述三步。
一些重点:
l state初始化为0,表示排他锁可用;变为1表示排他锁被占用。
l 业务类mutex.acquire()的实现委托给了sync.acquire()表明这是在排他获取资源。
l sync.tryAcquire()作为钩子方法在Sync内实现,它将被sync.acquire()(继承自AQS)这个模板方法调用。可见业务类委托给了模板方法,而模板方法调用了钩子方法。
l sync.tryAcquire()内部调用了继承自AbstractOwnableSynchronizer 的setExclusiveOwnerThread()方便地标记排他线程。
class Mutex implementsLock, java.io.Serializable {
// Our internal helper class
private static class Syncextends AbstractQueuedSynchronizer {
// Report whether in locked state
protected boolean isHeldExclusively(){
return getState() == 1;
}
// Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1;// Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Release the lock by setting stateto zero
protected booleantryRelease(int releases) {
assert releases == 1;// Otherwise unused
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provide a Condition
ConditionnewCondition() {
return new ConditionObject();
}
// Deserialize properly
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException{
s.defaultReadObject();
setState(0);// reset to unlocked state
}
}
// The sync object does all the hardwork. We just forward to it.
private final Sync sync =new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(longtimeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(timeout));
}
}
AQS内部细节
AQS实现了一个FIFO队列,用于管理等候在临界区外的线程。队列的每个节点保留了线程信息,因此线程可以安然休眠,待临界区的资源可用时再次唤醒。注意,队列的存在并不保证公平,因为AQS并不确保被唤醒的队首线程能进入临界区,唤醒过程中可能会被其它尚未入队的竞争线程抢占先机。
线程的休眠和唤醒是通过LockSupport.park()和LockSupport.unpark()实现的。LockSupport是整个concurrent包最底层的API之一。在AQS的注释中写到,如果你对其排队机制不满,可以重新定义队列的数据结构并通过LockSupport调度。
数据结构:双向链表构成的FIFO队列(链表是其物理结构,队列是其逻辑用途。注释中称其为CLH Lock队列的变种,而CLH Lock常用于实现自旋锁,其确保无饥饿性和公平性)
l 入队和出队针对的是节点(内部类Node),每个节点代表一个排队的线程。
l 头结点是一个哑元(dummy)节点,始终代表队列中最后一个进入了临界区的节点(线程)。
l next指针用于维护队列顺序;当临界区的资源被释放时,头结点通过next指针找到队首节点。
l prev指针用于在节点(线程)被取消时,让节点的上家直接指向下家完成出队动作(标准的链表操作)。
节点类型有2种:共享和排他,对应两种访问方式。
此处引用一张图来说明排他和共享访问的特点。一般说来,读操作允许共享访问,而写操作属于排他访问。图中绿色表示允许进入临界区,红色表示被block。这意味着,被读锁占用的临界区,后续的读可以立即进入,写需要等待;而被写锁占用的临界区,后续无论读写都必须等待。(假设并发读数量不设上限)
(图片引用自http://ifeve.com/introduce-abstractqueuedsynchronizer/)
节点状态有5种,与队列的通知机制紧密相关。
值 | 节点状态waitStatus | 用途 | 备注 |
0 | 非下述节点 | 默认状态。 |
|
1 | CANCELLED | 已取消。表示当前节点已放弃进入临界区。 |
|
-1 | SIGNAL | 发信号。当前节点在入队后、进入休眠状态前,应确保将其prev节点类型改为SIGNAL,以便后者取消或释放时将当前节点唤醒。 |
|
-2 | CONDITION |
| 条件队列专用。 |
-3 | PROPAGATE | 传播。由于共享访问的特点(上图),连续的读操作节点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。 | 共享专用 |
上述内容显得晦涩,为了更好理解,需要简化一下(抛开-2条件队列)并配合阅读下面的源码。要点是,排他的情况只需关注前三项-1,0,1;共享的情况再来看-3。
public finalvoid acquire(intarg) {
if (!tryAcquire(arg)&&
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
在排他获取资源之前,不允许被中断。其实现可简单描述为:通过tryAcquire()尝试获取,不成功便包装为排他节点入队。注意acquireQueued()方法进入临界区后才会返回,且返回的是中断标志位,若为true,则触发selfInterrupt()抛出InterruptedException。可见中断请求不是被忽略了,而是推迟响应。
final booleanacquireQueued(final Node node,intarg) {
boolean failed = true;
try {
booleaninterrupted =false;
for (;;) {
finalNode p = node.predecessor();
if(p == head && tryAcquire(arg)) {
setHead(node);
p.next= null;//help GC
failed = false;
returninterrupted;
}
if(shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally{
if (failed)
cancelAcquire(node);
}
}
注意无限循环代码块for (;;){…},下文中“无限for循环”均特指它。
无限循环的内容是,首先取当前节点的上家,若上家为头节点,表示当前节点在队首,有资格尝试获取资源。成功的话会将当前节点设为头结点(头节点始终代表队列中最后一个进入了临界区的节点),清理并结束。
若上家非头节点,通过shouldParkAfterFailedAcquire()确保上家的节点状态被置为SIGNAL(会在合适的时候唤醒当前节点),再通过parkAndCheckInterrupt()进入休眠状态。
private staticboolean shouldParkAfterFailedAcquire(Node pred, Nodenode) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set statusasking a release
* to signal it, so it can safelypark.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skipover predecessors and
* indicate retry.
*/
do {
node.prev= pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 orPROPAGATE. Indicate that we
* need a signal, but don't parkyet. Caller will need to
* retry to make sure it cannotacquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
若上家状态为SIGNAL,当前节点可安然入睡;
ws>0(只能是1 Cancelled)表示上家被取消,通过do…while循环,递归迭代链表使无效上家们出队。正好借此过程清理无效节点。
else其它情况,CAS确保上家状态置为SIGNAL。
此函数return true表示允许当前节点(线程)休眠。return false表示需要重入无限for循环检查(可能在修改状态的过程中当前节点已经来到了队首,那么在休眠之前再tryAcquire()一次,可提高性能)。
private finalboolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
由于“&&”短路表达式的关系,仅当shouldParkAfterFailedAcquire()返回true的前提下,parkAndCheckInterrupt()才会被执行。前者表示当前节点(线程)可入眠,后者执行入眠动作LockSupport.park(this);并在唤醒后返回中断标志位。
唤醒后即使中断标志为true,无限for循环仍将继续,也不会即时抛出异常。对比acquireInterruptibly()所依赖的doAcquireInterruptibly(),最大的区别就是唤醒后会根据中断标志即时抛出InterruptedException。
排他获取的重点是观察无限for循环代码块:仅当自己为队首时才尝试进入临界区;否则会在确保上家将会唤醒自己的前提下进入休眠状态。
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()成功,则试图唤醒队首(头节点的下家)
private void unparkSuccessor(Nodenode) {
/*
* If status is negative (i.e.,possibly needing signal) try
* to clear in anticipation ofsignalling. It is OK if this
* fails or if status is changed bywaiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node,ws, 0);
/*
* Thread to unpark is held insuccessor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to findthe actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null|| s.waitStatus > 0) {
s = null;
for(Node t = tail; t != null&& t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
当前节点(可以是头节点)状态为负(例如-1 SIGNAL),表示下家需要唤醒,则预先将其状态重置为0,表示已尝试唤醒。这一步无伤大雅,即使被新入队的节点改回去了也没关系。
正常情况下会直接唤醒下家;但当下家不可唤醒时(被取消或表面上为null),则尝试从对尾反向寻找可用节点作为下家后,再尝试唤醒。
这样做的原因是,入队操作始终先变更new.prev指针,再变更tail.next指针,且不是原子操作,因此中间状态时next指针可能为空(即所谓“表面上为null”)。
通过观察入队的源码进一步说明问题:
private Node enq(finalNode node) {
for (;;) {
Node t = tail;
if(t == null) { // Must initialize
if(compareAndSetHead(new Node()))
tail =head;
} else {
node.prev =t;
if (compareAndSetTail(t,node)) {
t.next =node;
returnt;
}
}
}
}
注意尾节点tail跟头结点head都是dummy节点,类似地,前者始终指向队尾节点。
(图片引用自http://ifeve.com/introduce-abstractqueuedsynchronizer/)
tail==null时执行初始化,else块中是真正的入队操作:
1、 新入队节点的上家指向原队尾节点;
2、 尝试把尾节点指向新入队节点;
3、 原尾节点的下家指向新入队节点;
(注意t始终是原队尾节点)
只要成功唤醒,线程便会重新进入无限for循环再次尝试获取资源。
共享获取&释放
掌握了前述内容后,会发现共享与排他在结构和内容上是雷同的。
来看看acquireShared()依赖的doAcquireShared()较之acquireQueued()有什么区别:
private voiddoAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
booleaninterrupted =false;
for(;;) {
finalNode p = node.predecessor();
if(p == head) {
int r= tryAcquireShared(arg);
if(r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;// help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p,node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if(failed)
cancelAcquire(node);
}
}
差异主要有二:
1、 入队的节点类型换成了共享,即Node.SHARED。
2、 setHead()换成了setHeadAndPropagate()。此处可以解释清楚节点状态-3PROPAGATE的用法。
private void setHeadAndPropagate(Nodenode,int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of thesechecks may cause
* unnecessary wake-ups, but only whenthere are multiple
* racing acquires/releases, so mostneed signals now or soon
* anyway.
*/
if (propagate > 0 || h ==null || h.waitStatus < 0) {
Node s = node.next;
if(s == null || s.isShared())
doReleaseShared();
}
}
满足if语句中的三个表达式之一,即可尝试传播(PROPAGATE),意即唤醒从队首开始一系列连续的共享节点,允许它们挨个进入临界区。
三个表达式作为准入条件貌似粗放,只要下家可能存在或已明示需要传播即可满足。例如参数propagate直接代入了tryAcquireShared(尝试共享获取)的返回值,后者与tryAcquire()(尝试排他获取)返回boolean表示成功与否所不同的是,它可以返回:
l 负数,表示获取失败;
l 0表示共享获取成功,但不允许后续共享节点继续获取;
l 正数则表示允许后续共享节点继续获取。
因此propagate > 0表示共享资源未达上限,后续共享节点可继续尝试获取。
而实现传播的关键是通过doReleaseShared(),该方并不释放资源,只是唤醒队首节点。然而队首节点被唤醒后立即尝试获取资源,成功后会再次触发setHeadAndPropagate(),形成多米诺骨牌的效果。注意,传播机制没有一次唤醒多个,而仅仅唤醒了队首的共享节点;节点被唤醒后也不保证能进入临界区,若获取失败(如共享资源数耗尽)可能再次入眠。
private voiddoReleaseShared() {
/*
* Ensure that a release propagates,even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor ofhead if it needs
* signal. But if it does not, statusis set to PROPAGATE to
* ensure that upon release,propagation continues.
* Additionally, we must loop in case anew node is added
* while we are doing this. Also,unlike other uses of
* unparkSuccessor, we need to know ifCAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if(h != null && h != tail) {
intws = h.waitStatus;
if(ws == Node.SIGNAL) {
if(!compareAndSetWaitStatus(h, Node.SIGNAL,0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h,0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if(h == head) // loop if head changed
break;
}
}
代码意图是,若队首节点存在,则唤醒它。ws == Node.SIGNAL 表示队首有节点在等候(入队节点会确保prev节点的状态为SIGNAL后入眠)
若无人排队,简单设头结点状态为PROPAGATE。
至此,SIGNAL特指有下家在排队待唤醒;而PROPAGATE表示当前无下家(无人排队),且新节点应立即尝试进入临界区。体会必胜客门前的指示牌“请在此排队”与“欢迎光临”的区别。0可以理解成没有指示牌。
在理解了Sync和AQS之后,最后再来看看究竟FutureTask通过怎样的机制分别唤醒了处于阻塞状态的查询线程。
再次回顾,FutureTask定义的state承载着任务的运行状态:0(初始)代表ready;1代表running;2代表ran;4代表cancelled。
其临界区仅允许共享访问。通常任务开始后,临界区即“关门大吉”(伪装成临界区满),后续所有查询线程依次入队,等待任务结束或取消。
protected int tryAcquireShared(int ignore) {
return innerIsDone() ? 1: -1;
}
细节是,每当查询线程尝试进入临界区,首先触发tryAcquireShared(),若任务未结束始终返回-1,表示共享获取失败,随即查询线程入队;否则返回1,表示共享获取成功,且允许后续线程继续获取。这样看来,共享访问的意义在于,当多个不同的查询线程进入队列,一旦任务结束,可径由“传播”的方式,迅速依次被唤醒。
查询线程被唤醒后,先检查自身的中断标志,并据此抛出中断异常。未被中断,则成功进入临界区、获取到共享资源(0,暗示忽略请求的资源数),退出了acquireSharedInterruptibly()内的无限for循环,于是检查任务的cancel状态,并据此抛出CancellationException。
V innerGet() throwsInterruptedException, ExecutionException {
acquireSharedInterruptibly(0);
if(getState() ==CANCELLED)
throw newCancellationException();
if (exception !=null)
throw newExecutionException(exception);
return result;
}
至此,文章开头的问题得出了答案