Java 并发(4)AbstractQueuedSynchronizer 源码分析之条件队列

转 https://mp.weixin.qq.com/s/-O2qJTOizvbl7gx1RtFGjQ


通过前面三篇的分析,我们深入了解了 AbstractQueuedSynchronizer 的内部结构和一些设计理念,知道了 AbstractQueuedSynchronizer 内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列和条件队列。

我们还是拿公共厕所做比喻,同步队列是主要的排队区,如果公共厕所没开放,所有想要进入厕所的人都得在这里排队。而条件队列主要是为条件等待设置的,我们想象一下如果一个人通过排队终于成功获取锁进入了厕所,但在方便之前发现自己没带手纸,碰到这种情况虽然很无奈,但是它也必须接受这个事实,这时它只好乖乖的出去先准备好手纸 (进入条件队列等待),当然在出去之前还得把锁给释放了好让其他人能够进来,在准备好了手纸 (条件满足) 之后它又得重新回到同步队列中去排队。

当然进入房间的人并不都是因为没带手纸,可能还有其他一些原因必须中断操作先去条件队列中去排队,所以条件队列可以有多个,依不同的等待条件而设置不同的条件队列。条件队列是一条单向链表,Condition 接口定义了条件队列中的所有操作,AbstractQueuedSynchronizer 内部的 ConditionObject 类实现了 Condition 接口。

下面我们看看 Condition 接口都定义了哪些操作。

publicinterfaceCondition{

//响应线程中断的条件等待

voidawait()throwsInterruptedException;

//不响应线程中断的条件等待

voidawaitUninterruptibly();

//设置相对时间的条件等待(不进行自旋)

longawaitNanos(longnanosTimeout)throwsInterruptedException;

//设置相对时间的条件等待(进行自旋)

booleanawait(longtime, TimeUnit unit)throwsInterruptedException;

//设置绝对时间的条件等待

booleanawaitUntil(Date deadline)throwsInterruptedException;

//唤醒条件队列中的头结点

voidsignal();

//唤醒条件队列的所有结点

voidsignalAll();

}

Condition 接口虽然定义了这么多方法,但总共就分为两类,以 await 开头的是线程进入条件队列等待的方法,以 signal 开头的是将条件队列中的线程 “唤醒” 的方法。这里要注意的是,调用 signal 方法可能唤醒线程也可能不会唤醒线程,什么时候会唤醒线程这得看情况,后面会讲到,但是调用 signal 方法一定会将线程从条件队列中移到同步队列尾部。

这里为了叙述方便,我们先暂时不纠结这么多,统一称 signal 方法为唤醒条件队列线程的操作。大家注意看一下,await 方法分为 5 种,分别是响应线程中断等待,不响应线程中断等待,设置相对时间不自旋等待,设置相对时间自旋等待,设置绝对时间等待;signal 方法只有 2 种,分别是只唤醒条件队列头结点和唤醒条件队列所有结点的操作。

同一类的方法基本上是相通的,由于篇幅所限,我们不可能也不需要将这些方法全部仔细的讲到,只需要将一个代表方法搞懂了再看其他方法就能够触类旁通。所以在本文中我只会细讲 await 方法和 signal 方法,其他方法不细讲但会贴出源码来以供大家参考。

1. 响应线程中断的条件等待

//响应线程中断的条件等待

publicfinalvoidawait()throwsInterruptedException{

//如果线程被中断则抛出异常

if(Thread.interrupted()) {

thrownewInterruptedException();

}

//将当前线程添加到条件队列尾部

Node node = addConditionWaiter();

//在进入条件等待之前先完全释放锁

intsavedState = fullyRelease(node);

intinterruptMode =0;

//线程一直在while循环里进行条件等待

while(!isOnSyncQueue(node)) {

//进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:

//1.同步队列的前继结点已取消

//2.设置同步队列的前继结点的状态为SIGNAL失败

//3.前继结点释放锁后唤醒当前结点

LockSupport.park(this);

//当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待, 此时需要将结点移出条件队列

if((interruptMode = checkInterruptWhileWaiting(node)) !=0) {

break;

}

}

//线程醒来后就会以独占模式获取锁

if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {

interruptMode = REINTERRUPT;

}

//这步操作主要为防止线程在signal之前中断而导致没与条件队列断绝联系

if(node.nextWaiter !=null) {

unlinkCancelledWaiters();

}

//根据中断模式进行响应的中断处理

if(interruptMode !=0) {

reportInterruptAfterWait(interruptMode);

}

}

当线程调用 await 方法的时候,首先会将当前线程包装成 node 结点放入条件队列尾部。在 addConditionWaiter 方法中,如果发现条件队列尾结点已取消就会调用 unlinkCancelledWaiters 方法将条件队列所有的已取消结点清空。

这步操作是插入结点的准备工作,那么确保了尾结点的状态也是 CONDITION 之后,就会新建一个 node 结点将当前线程包装起来然后放入条件队列尾部。注意,这个过程只是将结点添加到同步队列尾部而没有挂起线程哦。

第二步:完全将锁释放

//完全释放锁

finalintfullyRelease(Node node){

booleanfailed =true;

try{

//获取当前的同步状态

intsavedState = getState();

//使用当前的同步状态去释放锁

if(release(savedState)) {

failed =false;

//如果释放锁成功就返回当前同步状态

returnsavedState;

}else{

//如果释放锁失败就抛出运行时异常

thrownewIllegalMonitorStateException();

}

}finally{

//保证没有成功释放锁就将该结点设置为取消状态

if(failed) {

node.waitStatus = Node.CANCELLED;

}

}

}

将当前线程包装成结点添加到条件队列尾部后,紧接着就调用 fullyRelease 方法释放锁。注意,方法名为 fullyRelease 也就这步操作会完全的释放锁,因为锁是可重入的,所以在进行条件等待前需要将锁全部释放了,不然的话别人就获取不了锁了。如果释放锁失败的话就会抛出一个运行时异常,如果成功释放了锁的话就返回之前的同步状态。

第三步:进行条件等待

//线程一直在while循环里进行条件等待

while(!isOnSyncQueue(node)) {

//进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:

//1.同步队列的前继结点已取消

//2.设置同步队列的前继结点的状态为SIGNAL失败

//3.前继结点释放锁后唤醒当前结点

LockSupport.park(this);

//当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待, 此时需要将结点移出条件队列

if((interruptMode = checkInterruptWhileWaiting(node)) !=0) {

break;

}

}

//检查条件等待时的线程中断情况

privateintcheckInterruptWhileWaiting(Node node){

//中断请求在signal操作之前:THROW_IE

//中断请求在signal操作之后:REINTERRUPT

//期间没有收到任何中断请求:0

returnThread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;

}

//将取消条件等待的结点从条件队列转移到同步队列中

finalbooleantransferAfterCancelledWait(Node node){

//如果这步CAS操作成功的话就表明中断发生在signal方法之前

if(compareAndSetWaitStatus(node, Node.CONDITION,0)) {

//状态修改成功后就将该结点放入同步队列尾部

enq(node);

returntrue;

}

//到这里表明CAS操作失败, 说明中断发生在signal方法之后

while(!isOnSyncQueue(node)) {

//如果sinal方法还没有将结点转移到同步队列, 就通过自旋等待一下

Thread.yield();

}

returnfalse;

}

在以上两个操作完成了之后就会进入 while 循环,可以看到 while 循环里面首先调用 LockSupport.park (this) 将线程挂起了,所以线程就会一直在这里阻塞。在调用 signal 方法后仅仅只是将结点从条件队列转移到同步队列中去,至于会不会唤醒线程需要看情况。

如果转移结点时发现同步队列中的前继结点已取消,或者是更新前继结点的状态为 SIGNAL 失败,这两种情况都会立即唤醒线程,否则的话在 signal 方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前继结点来唤醒。当然,线程阻塞在这里除了可以调用 signal 方法唤醒之外,线程还可以响应中断,如果线程在这里收到中断请求就会继续往下执行。

可以看到线程醒来后会马上检查是否是由于中断唤醒的还是通过 signal 方法唤醒的,如果是因为中断唤醒的同样会将这个结点转移到同步队列中去,只不过是通过调用 transferAfterCancelledWait 方法来实现的。最后执行完这一步之后就会返回中断情况并跳出 while 循环。

第四步:结点移出条件队列后的操作

//线程醒来后就会以独占模式获取锁

if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {

interruptMode = REINTERRUPT;

}

//这步操作主要为防止线程在signal之前中断而导致没与条件队列断绝联系

if(node.nextWaiter !=null) {

unlinkCancelledWaiters();

}

//根据中断模式进行响应的中断处理

if(interruptMode !=0) {

reportInterruptAfterWait(interruptMode);

}

//结束条件等待后根据中断情况做出相应处理

privatevoidreportInterruptAfterWait(intinterruptMode)throwsInterruptedException{

//如果中断模式是THROW_IE就抛出异常

if(interruptMode == THROW_IE) {

thrownewInterruptedException();

//如果中断模式是REINTERRUPT就自己挂起

}elseif(interruptMode == REINTERRUPT) {

selfInterrupt();

}

}

当线程终止了 while 循环也就是条件等待后,就会回到同步队列中。不管是因为调用 signal 方法回去的还是因为线程中断导致的,结点最终都会在同步队列中。这时就会调用 acquireQueued 方法执行在同步队列中获取锁的操作,这个方法我们在独占模式这一篇已经详细的讲过。

也就是说,结点从条件队列出来后又是乖乖的走独占模式下获取锁的那一套,等这个结点再次获得锁之后,就会调用 reportInterruptAfterWait 方法来根据这期间的中断情况做出相应的响应。如果中断发生在 signal 方法之前,interruptMode 就为 THROW_IE,再次获得锁后就抛出异常;如果中断发生在 signal 方法之后,interruptMode 就为 REINTERRUPT,再次获得锁后就重新中断。

2. 不响应线程中断的条件等待

//不响应线程中断的条件等待

publicfinalvoidawaitUninterruptibly(){

//将当前线程添加到条件队列尾部

Node node = addConditionWaiter();

//完全释放锁并返回当前同步状态

intsavedState = fullyRelease(node);

booleaninterrupted =false;

//结点一直在while循环里进行条件等待

while(!isOnSyncQueue(node)) {

//条件队列中所有的线程都在这里被挂起

LockSupport.park(this);

//线程醒来发现中断并不会马上去响应

if(Thread.interrupted()) {

interrupted =true;

}

}

if(acquireQueued(node, savedState) || interrupted) {

//在这里响应所有中断请求, 满足以下两个条件之一就会将自己挂起

//1.线程在条件等待时收到中断请求

//2.线程在acquireQueued方法里收到中断请求

selfInterrupt();

}

}

3. 设置相对时间的条件等待 (不进行自旋)

//设置定时条件等待(相对时间), 不进行自旋等待

publicfinallongawaitNanos(longnanosTimeout)throwsInterruptedException{

//如果线程被中断则抛出异常

if(Thread.interrupted()) {

thrownewInterruptedException();

}

//将当前线程添加到条件队列尾部

Node node = addConditionWaiter();

//在进入条件等待之前先完全释放锁

intsavedState = fullyRelease(node);

longlastTime = System.nanoTime();

intinterruptMode =0;

while(!isOnSyncQueue(node)) {

//判断超时时间是否用完了

if(nanosTimeout <=0L) {

//如果已超时就需要执行取消条件等待操作

transferAfterCancelledWait(node);

break;

}

//将当前线程挂起一段时间, 线程在这期间可能被唤醒, 也可能自己醒来

LockSupport.parkNanos(this, nanosTimeout);

//线程醒来后先检查中断信息

if((interruptMode = checkInterruptWhileWaiting(node)) !=0) {

break;

}

longnow = System.nanoTime();

//超时时间每次减去条件等待的时间

nanosTimeout -= now - lastTime;

lastTime = now;

}

//线程醒来后就会以独占模式获取锁

if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {

interruptMode = REINTERRUPT;

}

//由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍

if(node.nextWaiter !=null) {

unlinkCancelledWaiters();

}

//根据中断模式进行响应的中断处理

if(interruptMode !=0) {

reportInterruptAfterWait(interruptMode);

}

//返回剩余时间

returnnanosTimeout - (System.nanoTime() - lastTime);

}

4. 设置相对时间的条件等待 (进行自旋)

//设置定时条件等待(相对时间), 进行自旋等待

publicfinalbooleanawait(longtime, TimeUnit unit)throwsInterruptedException{

if(unit ==null) {thrownewNullPointerException(); }

//获取超时时间的毫秒数

longnanosTimeout = unit.toNanos(time);

//如果线程被中断则抛出异常

if(Thread.interrupted()) {thrownewInterruptedException(); }

//将当前线程添加条件队列尾部

Node node = addConditionWaiter();

//在进入条件等待之前先完全释放锁

intsavedState = fullyRelease(node);

//获取当前时间的毫秒数

longlastTime = System.nanoTime();

booleantimedout =false;

intinterruptMode =0;

while(!isOnSyncQueue(node)) {

//如果超时就需要执行取消条件等待操作

if(nanosTimeout <=0L) {

timedout = transferAfterCancelledWait(node);

break;

}

//如果超时时间大于自旋时间, 就将线程挂起一段时间

if(nanosTimeout >= spinForTimeoutThreshold) {

LockSupport.parkNanos(this, nanosTimeout);

}

//线程醒来后先检查中断信息

if((interruptMode = checkInterruptWhileWaiting(node)) !=0) {

break;

}

longnow = System.nanoTime();

//超时时间每次减去条件等待的时间

nanosTimeout -= now - lastTime;

lastTime = now;

}

//线程醒来后就会以独占模式获取锁

if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {

interruptMode = REINTERRUPT;

}

//由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍

if(node.nextWaiter !=null) {

unlinkCancelledWaiters();

}

//根据中断模式进行响应的中断处理

if(interruptMode !=0) {

reportInterruptAfterWait(interruptMode);

}

//返回是否超时标志

return!timedout;

}

5. 设置绝对时间的条件等待

//设置定时条件等待(绝对时间)

publicfinalbooleanawaitUntil(Date deadline)throwsInterruptedException{

if(deadline ==null) {thrownewNullPointerException(); }

//获取绝对时间的毫秒数

longabstime = deadline.getTime();

//如果线程被中断则抛出异常

if(Thread.interrupted()) {thrownewInterruptedException(); }

//将当前线程添加到条件队列尾部

Node node = addConditionWaiter();

//在进入条件等待之前先完全释放锁

intsavedState = fullyRelease(node);

booleantimedout =false;

intinterruptMode =0;

while(!isOnSyncQueue(node)) {

//如果超时就需要执行取消条件等待操作

if(System.currentTimeMillis() > abstime) {

timedout = transferAfterCancelledWait(node);

break;

}

//将线程挂起一段时间, 期间线程可能被唤醒, 也可能到了点自己醒来

LockSupport.parkUntil(this, abstime);

//线程醒来后先检查中断信息

if((interruptMode = checkInterruptWhileWaiting(node)) !=0) {

break;

}

}

//线程醒来后就会以独占模式获取锁

if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {

interruptMode = REINTERRUPT;

}

//由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍

if(node.nextWaiter !=null) {

unlinkCancelledWaiters();

}

//根据中断模式进行响应的中断处理

if(interruptMode !=0) {

reportInterruptAfterWait(interruptMode);

}

//返回是否超时标志

return!timedout;

}

6. 唤醒条件队列中的头结点

//唤醒条件队列中的下一个结点

publicfinalvoidsignal(){

//判断当前线程是否持有锁

if(!isHeldExclusively()) {

thrownewIllegalMonitorStateException();

}

Node first = firstWaiter;

//如果条件队列中有排队者

if(first !=null) {

//唤醒条件队列中的头结点

doSignal(first);

}

}

//唤醒条件队列中的头结点

privatevoiddoSignal(Node first){

do{

//1.将firstWaiter引用向后移动一位

if( (firstWaiter = first.nextWaiter) ==null) {

lastWaiter =null;

}

//2.将头结点的后继结点引用置空

first.nextWaiter =null;

//3.将头结点转移到同步队列, 转移完成后有可能唤醒线程

//4.如果transferForSignal操作失败就去唤醒下一个结点

}while(!transferForSignal(first) && (first = firstWaiter) !=null);

}

//将指定结点从条件队列转移到同步队列中

finalbooleantransferForSignal(Node node){

//将等待状态从CONDITION设置为0

if(!compareAndSetWaitStatus(node, Node.CONDITION,0)) {

//如果更新状态的操作失败就直接返回false

//可能是transferAfterCancelledWait方法先将状态改变了, 导致这步CAS操作失败

returnfalse;

}

//将该结点添加到同步队列尾部

Node p = enq(node);

intws = p.waitStatus;

if(ws >0|| !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {

//出现以下情况就会唤醒当前线程

//1.前继结点是取消状态

//2.更新前继结点的状态为SIGNAL操作失败

LockSupport.unpark(node.thread);

}

returntrue;

}

可以看到 signal 方法最终的核心就是去调用 transferForSignal 方法,在 transferForSignal 方法中首先会用 CAS 操作将结点的状态从 CONDITION 设置为 0,然后再调用 enq 方法将该结点添加到同步队列尾部。

我们再看到接下来的 if 判断语句,这个判断语句主要是用来判断什么时候会去唤醒线程,出现这两种情况就会立即唤醒线程,一种是当发现前继结点的状态是取消状态时,还有一种是更新前继结点的状态失败时。

这两种情况都会马上去唤醒线程,否则的话就仅仅只是将结点从条件队列中转移到同步队列中就完了,而不会立马去唤醒结点中的线程。signalAll 方法也大致类似,只不过它是去循环遍历条件队列中的所有结点,并将它们转移到同步队列,转移结点的方法也还是调用 transferForSignal 方法。

7. 唤醒条件队列的所有结点

//唤醒条件队列后面的全部结点

publicfinalvoidsignalAll(){

//判断当前线程是否持有锁

if(!isHeldExclusively()) {

thrownewIllegalMonitorStateException();

}

//获取条件队列头结点

Node first = firstWaiter;

if(first !=null) {

//唤醒条件队列的所有结点

doSignalAll(first);

}

}

//唤醒条件队列的所有结点

privatevoiddoSignalAll(Node first){

//先把头结点和尾结点的引用置空

lastWaiter = firstWaiter =null;

do{

//先获取后继结点的引用

Node next = first.nextWaiter;

//把即将转移的结点的后继引用置空

first.nextWaiter =null;

//将结点从条件队列转移到同步队列

transferForSignal(first);

//将引用指向下一个结点

first = next;

}while(first !=null);

}

至此,我们整个的 AbstractQueuedSynchronizer 源码分析就结束了,相信通过这四篇的分析,大家能更好的掌握并理解 AQS。

这个类确实很重要,因为它是其他很多同步类的基石,由于笔者水平和表达能力有限,如果哪些地方没有表述清楚的,或者理解不到位的,还请广大读者们能够及时指正,共同探讨学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容