• 并发编程学习笔记(十六、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,没啥特殊的流程。

    AQS总结:

    1、AQS:AbstractQueuedSynchronize

    2、分为同步队列和条件队列

    同步队列又分为独占模式和共享模式,它们之间最主要的区别就是在同一时刻是否能有多个线程获取同步状态。

    • 独占:acquire()、release()。
    • 共享:acquireShared()、releaseShared()。

    实现的主要区别:拿到资源后是否会唤醒后继节点获取资源。

    3、条件队列,一般情况下它与同步队列是相互独立的,没有联系。但当其被唤醒的时候会将其加入到同步队列,让它和其它普通线程一样去竞争锁。

    主要涉及方法:await()、signal()。

    4、实现原理对比:

      同步队列排它锁 同步队列共享锁 条件队列
    加锁
    • 先尝试获取锁,获取不到就通过自旋的方式加入到同步队列队尾。
    • 并且会在自己为head节点的后继节点时竞争锁。
    • 当自己非head节点的后继节点或竞争锁失败时,检已取消的节点并检查中断状态,若需要则阻塞线程。
    • 它不响应中断,而是维护中断的标识。
    • 与排它锁不一样的是,排它锁拿到资源后不会唤醒队友,而共享锁会,也就是调用doReleaseShared()释放资源其。
    • 它逻辑类似。
     
    • 先加入到条件队列中,之后释放锁然后等待唤醒。
    • 区别在于同步队列入队前是无锁的,而条件队列是要有锁的。
    • 区分是同步队列还是条件队列的条件是,waitStatus、prev、next指针。
    解锁
    • 尝试获取锁,若获取成功则唤醒当前线程的后继节点。
    • 同样的在尝试获取到锁之后,会唤醒,不同的是唤醒的对象不仅是自己的后继节点,还有队列中其它状态为SIGNAL的节点。
    • 差不多咯
  • 相关阅读:
    【IDEA配置】web项目报错404 login.html找不到资源或无法访问
    完成一个IDEA web项目(二)登录功能实现
    完成一个IDEA web项目(一)项目搭建准备工作
    Servlet中写了注解@WebServlet但访问servlet报404错误
    Category分类测试报错:Category annotations on Parameterized classes are not supported on individual methods.
    Junit测试报错:java.lang.AssertionError at org.junit.Assert.assertTrue
    集合Set添加多个元素
    【IDEA配置】IDEA新建maven web项目
    【IDEA配置】IDEA新建web项目
    JSON
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/13171743.html
Copyright © 2020-2023  润新知