并发编程学习笔记(十六、AQS同步器源码解析5,AQS条件锁Condition实现原理2)

目录:

  • await()第二部分解析
  • 如何处理线程被唤醒到竞争到锁的这段时间发生的中断
  • AQS总结

await()第二部分解析

之前我们说到线程在接收到signal()的通知后会从调用await()之处执行,但这里需要注意的是我们被唤醒的时候,其实并不知道是因为什么原因被唤醒的

  • 有可能是其它线程调用了signal()方法
  • 也有可能是当前线程被中断了。

但无论是哪种方式的唤醒,最终线程都会从condition queue到sync queue中,并且在sync queue中利用acquireQueued()方法进行阻塞式的竞争锁,抢不到就挂起。

所以当await()方法返回时必然是保证当前线程是已经获取到锁了的

——————————————————————————————————————————————————————————————————————

那么这里我们就需要注意一个问题,如果从线程被唤醒到竞争到锁的这段时间发生了中断该如何处理?

前面我们说到中断对于当前线程只是一个建议,而具体如何做是交当前线程自行处理。而acquireQueued()也是一样,它不响应中断,而是记录中断的状态并交由上层来处理,也就是await()来处理。

那await()是如何处理的呢,这取决于中断发生时是否被signal过,所以这里就会分为两种情况:

  • 中断发生时,线程还未被signal过。
  • 中断发生时,线程已经被signal过。

1、中断发生时,线程还未被signal过:

此时说明当前线程还处于condition queue中,此时正常等待行为被打断,因此需要在await()方法返回后抛出InterruptdException,表示当前线程因中断被唤醒。

2、中断发生时,线程已经被signal过:

此时线程已经被signal过了,说明这个中断来的太晚了,我已经被唤醒了你的中断指令才到,我没必要理你,直接忽略。我只需要在await()方法结束后自行中断下,补下这中断状态即可。

就好比你去酒店吃饭,饭都吃完了有一个菜还没上,此时你就去问服务员菜做了没,没做就不要了,然后服务员去后厨之后回来说不好意思已经下锅了。这里的菜不要了(发起的中断)就是指中断来的太晚了菜已经下锅了就是已经被signal()了

——————————————————————————————————————————————————————————————————————

概念清楚了后我们来看看await()是如何做的:

 1 public final void await() throws InterruptedException {
 2     if (Thread.interrupted())
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();
 5     int savedState = fullyRelease(node);
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {
 8         LockSupport.park(this);
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;
11     }
12     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13         interruptMode = REINTERRUPT;
14     if (node.nextWaiter != null) // clean up if cancelled
15         unlinkCancelledWaiters();
16     if (interruptMode != 0)
17         reportInterruptAfterWait(interruptMode);
18 }
1 /** Mode meaning to reinterrupt on exit from wait */
2 private static final int REINTERRUPT =  1;
3 /** Mode meaning to throw InterruptedException on exit from wait */
4 private static final int THROW_IE    = -1;

它通过interruptMode中断模式来记录中断时间,该变量有三个值:

  • 0:代表整个过程中一直没有中断发生。
  • THROW_IE:表示退出await()方法时需要抛出InteruptedException,这种模式对应于中断发生在signal之前。
  • REINTERRUPT:表示退出await()方法时只需要再自我中断以下, 这种模式对应于中断发生在signal之后, 即中断来的太晚了。

如何处理线程被唤醒到竞争到锁的这段时间发生的中断

之前我们说到分为两种情况,那么我这里来说下await()是如何处理这两种情况的:

  • 中断发生时,线程还未被signal过。
  • 中断发生时,线程已经被signal过。

在说明前我先讲主流程的代码贴出来方便你查阅:

 1 public final void await() throws InterruptedException {
 2     /*if (Thread.interrupted())
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();
 5     int savedState = fullyRelease(node);
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {
 8         LockSupport.park(this);*/
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;
11     //}
12     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13         interruptMode = REINTERRUPT;
14     if (node.nextWaiter != null) // clean up if cancelled
15         unlinkCancelledWaiters();
16     if (interruptMode != 0)
17         reportInterruptAfterWait(interruptMode);
18 }

1、中断先于signal:

线程被唤醒后,首先会使用checkInterruptWhileWaiting来获取中断模式:

1 private int checkInterruptWhileWaiting(Node node) {
2     return Thread.interrupted() ?
3         (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
4         0;
5 }

我们这次讨论的情况是中断先于signal,也就是说Thread.interrupted()肯定是为true的,也就是会走到transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT

 1 final boolean transferAfterCancelledWait(Node node) {
 2     // CAS将当前节点的waitStatus由CONDITION变为0
 3     if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
 4         enq(node);
 5         return true;
 6     }
 7     while (!isOnSyncQueue(node))
 8         Thread.yield();
 9     return false;
10 }

我们知道如果线程被signal过,那么肯定会从条件队列变为同步队列,而此时还未执行signal,所以第3行代码一定会成功。然后将当前节点自旋到同步队列队尾,并返回true。

这里有一点需要注意下,虽然当前线程已经从同步队列到了条件队列,但它的nextWaiter的指针还是未断开的

综上所述,此中情况下,checkInterruptWhileWaiting()最终的返回值是THROW_IE,也就是-1。

表示线程非正常唤醒,而是被中断的,需要抛出InterruptedException。

——————————————————————————————————————————————————————————————————————

那么我们现在回到await()方法,此时interruptMode的值为-1,我们来看看后续的代码会如何执行。

1 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
2     interruptMode = REINTERRUPT;
3 if (node.nextWaiter != null) // clean up if cancelled
4     unlinkCancelledWaiters();
5 if (interruptMode != 0)
6     reportInterruptAfterWait(interruptMode);

首先若interruptMode不是THROW_IE的话,会阻塞式的获取锁,但因为线程是被中断的,所以此if跳过。

然后我们来看下第二段,node.nextWaiter != null;你还记得上面我们提到的当前线程只是自旋加入同步队列队尾,但并未断开nextWaiter嘛,所以这个地方会执行unlinkCancelledWaiters()来移除所有状态是取消了的线程。

那么为我们会这样做呢,因为此时线程已经被唤醒需要从条件队列里移除,由于条件队列是一个单向链表,所以要移除一个节点必须要遍历整个队列。因为是不可避免的需要遍历整个队列,所以这里干脆调用unlinkCancelledWaiters()把队列中所有waitStatus非CONDITION都给剔除。

最后,interruptMode非0时上报下中断状态。

1 private void reportInterruptAfterWait(int interruptMode)
2     throws InterruptedException {
3     if (interruptMode == THROW_IE)
4         throw new InterruptedException();
5     else if (interruptMode == REINTERRUPT)
6         selfInterrupt();
7 }

为异常中断则抛出异常,正常退出的话再维护下中断标识。

——————————————————————————————————————————————————————————————————————

2、中断后于signal:

这种情况对应中断来的太晚了,及REINTERRUPT,也就是拿到锁退出await()方法前只需要自我中断下,而不需要抛出InterruptedException。

同样的,这种情况也分为两种

  • 被唤醒时已经发生了中断,但此时线程已经被signal过了。
  • 唤醒时未发生中断,但在抢锁的过程中发生了中断。

1、被唤醒时已经发生了中断,但此时线程已经被signal过了:

这种情况和上述差不多,主要差别就是方法返回了false

1 final boolean transferAfterCancelledWait(Node node) {
2     if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
3         enq(node);
4         return true;
5     }
6     while (!isOnSyncQueue(node))
7         Thread.yield();
8     return false;
9 }

由于此时是中断后于signal,所以此时执行第2行代码肯定是false,因为当前线程已经进入同步队列了,状态不是CONDITION。

但其实当前线程也可能是在进入同步队列的路上,为什么这么说呢,我来解释下:

  • 假设有线程A、线程B是并发执行的。
  • 线程A被唤醒后检测到发生了中断,所以走到了transferAfterCancelledWait。
  • 而线程B在这之前已经调用了signal方法,该方法会调用transferForSignal将当前线程加入到同步队列队尾。
  • 这里我们分析的是中断在signal之后,所以此时线程B的compareAndSetWaitStatus会优先于线程A执行。
  • 所以这里可能会出现B已经修改了node的waitStatus状态,但还未来得及调用enq方法,线程A就执行了transferAfterCancelledWait。
  • 此时发现waitStatus已经不是Condition了,但其实自己还没有添加到同步队列中去,因此它接下来会通过自旋等待线程B执行完transferForSignal方法。

根本原因:两个线程并发执行的时候另一个线程会通过signal().doSignal(first).transferForSignal(first).compareAndSetWaitStatus(node, Node.CONDITION, 0)方法修改当前线程的waitStatus,而transferAfterCancelledWait()只有在waitStatus为Condition时才会调用enq自旋的加入同步队列队尾,所以会一直调用isOnSyncQueue()自旋的判断当前节点是否已经是同步队列(此时的线程就是在进入同步队列的路上,而等待的就是另一个线程执行完transferForSignal方法进入同步队列队尾)。

1 final boolean transferForSignal(Node node) {
2     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
3         return false;
4     Node p = enq(node);
5     int ws = p.waitStatus;
6     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
7         LockSupport.unpark(node.thread);
8     return true;
9 }

线程A会不断的通过isOnSyncQueue判断节点是否已经同步到了同步队列中去,直到同步完成后执行await()剩余逻辑。

1 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
2     interruptMode = REINTERRUPT;
3 if (node.nextWaiter != null) // clean up if cancelled
4     unlinkCancelledWaiters();
5 if (interruptMode != 0)
6     reportInterruptAfterWait(interruptMode);

2、唤醒时未发生中断,但在抢锁的过程中发生了中断:

其实这种情况非常简单,唯一的区别在与是抢锁的过程中发生了中断而不是唤醒的过程中,也就是下面这行代码:

1 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
2     interruptMode = REINTERRUPT;

我们知道acquireQueued是不响应中断,只是维护中断的标识,所以如果是抢锁的过程中发生了中断acquireQueued是为true的,拿interruptMode值是多少呢,我们再来回归下await方法。

 1 public final void await() throws InterruptedException {
 2     if (Thread.interrupted())
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();
 5     int savedState = fullyRelease(node);
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {
 8         LockSupport.park(this);
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;
11     }
12     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13         interruptMode = REINTERRUPT;
14     if (node.nextWaiter != null) // clean up if cancelled
15         unlinkCancelledWaiters();
16     if (interruptMode != 0)
17         reportInterruptAfterWait(interruptMode);
18 }

可见interruptMode一开始是0,而线程在7-11行是为中断的,所以checkInterruptWhileWaiting会返回0,不满足条件,还会据需执行第7行的isOnSyncQueue。

而我们知道此情况是已经调用signal唤醒线程了的,所以当前线程肯定是在同步队列中的,isOnSyncQueue返回true,while循环结束,也就是说interruptMode还是0。

综上所述,acquireQueued = true,interruptMode = 0,所以最后到了第13行,interruptMode最终的结果是REINTERRUPT。

这也就是中断来的太晚的一种情况,此时只需要再自我中断下即可(17行的reportInterruptAfterWait)。

——————————————————————————————————————————————————————————————————————

至此中断的各种情况就已经分析完了,就只剩正常的中断一直未发生的情况,这种情况也是异常的简单,interruptMode至此至终就是0,没啥特殊的流程。

猜你喜欢

转载自www.cnblogs.com/bzfsdr/p/13171743.html