Java之AQS(三)

AQS


前言

上篇文章 中,我们分析了同步队列的节点如何new出条件队列,条件队列节点又是如何跑到同步队列中去的。

而在分析同步队列的节点如何new出条件队列的时候,我们从 await方法开始分析,直到了 isOnSyncQueue方法。而本篇内容将接着继续分析,挂起的线程唤醒后的过程。

cpp 复制代码
public final void await() throws InterruptedException {
            // 如果当前线程被中断过, 则直接抛出中断异常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 封装当前线程, 并扔到条件队列中
            Node node = addConditionWaiter();
            // 完全释放当前线程占用的锁, 并保存释放前(即当前)的锁状态
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果当前节点(封装好的线程)不在同步队列中
            // 说明还没有被signal过
            while (!isOnSyncQueue(node)) {
                // 挂起
                LockSupport.park(this);
                // 讲道理, 上面执行后不应该执行到这里的
                // 当如果线程被中断了、或者被signal了, 
                // 则会跑到这里
                // 检查换新的原因, 如果是中断则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 下面这部分代码先不用管
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

阻塞的线程醒过来了

当条件队列的节点不在同步队列时,我们接下的操作是计划将当前线程挂起,既然挂起了,下面的程序又是什么时候执行呢?

cpp 复制代码
  while (!isOnSyncQueue(node)) {
                // 挂起
                LockSupport.park(this);
                // 讲道理, 上面执行后不应该执行到这里的
                // 当如果线程被中断了、或者被signal了, 
                // 则会跑到这里
                // 检查唤醒的原因, 如果是中断则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
     interruptMode = REINTERRUPT;
 if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
  if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);

这里有两种情况:

1.没有中断、有对应的ConditionObject调用了signal方法进行唤醒操作。

  1. 休眠的线程被中断了、被迫唤醒。

checkInterruptWhileWaiting

所以,这里既然醒了,我们需要检查一下醒来的原因,以便判断接下的路该如何走。于是就有了这一行代码的判断:

cpp 复制代码
 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

关于checkInterruptWhileWaiting方法的源码如下,它会判断当前线程是否发生中断,没有的话取值为0(被正常的signal),有的话,需要检查一下中断的原因。

cpp 复制代码
 /**
   * Checks for interrupt, returning THROW_IE if interrupted
   * before signalled, REINTERRUPT if after signalled, or
   * 0 if not interrupted.
 */
   private int checkInterruptWhileWaiting(Node node) {
      return Thread.interrupted() ?
         (transferAfterCancelledWait(node) ? 
         THROW_IE : REINTERRUPT) : 0;
   }

interruptMode = 0

先说说interruptMode 为0的情况吧,如果为0,没有中断,继续外层 的判断,即while (!isOnSyncQueue(node))

为什么需要上面的判断呢?根据我们前面对signal方法的分析,唤醒的线程可能还没来得及跑到同步队列中,也就是说线程可能处于"假唤醒"的状态,处理的方式也比较简单,就加上while (!isOnSyncQueue(node))的循环判断,正常醒来的线程没有在同步队列中,那就仍然在等待队列中,继续让它睡觉。

如果已经在同步队列中了,跳出while循环、然后执行如下部分的源码,前面分析acquireQueued时候,我们可以知道它就是 阻塞式的获取锁。

如果acquireQueued返回false表示没有发生中断,如果返回true则表示在抢锁的过程中发生了中断。当然了,由于interruptMode == 0这里interruptMode = REINTERRUPT会被执行到。

cpp 复制代码
 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
     interruptMode = REINTERRUPT;
 if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
  if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);

接着判断如果node.nextWaiter不为null,会清除已经处于Cancel状态的节点。最后,在源码中进行自我中断。

cpp 复制代码
 private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

interruptMode != 0

interruptMode != 0表示有中断发生,根据如下源码的注释,我们可以知道,中断也是需要分为两种情况的,一种是在唤醒signal之前发生中断,此时将interruptMode 设置为THROW_IE ,另一种则是在signal之后(已发生signal)发生中断,此时将interruptMode 设置为REINTERRUPT 。

这里可能比较难理解,很多人觉得如果是signal后,那么Thread.interrupted() ?判断时不应该返回0吗。其实这种思考陷入了一个单线程的思维模式里。多线程之下,A调用signa和B发生中断,是两个维度的东西,没有商量的情况下,谁也不能保证B的中断是发生在A的signal之前、还是之后。

关于interruptMode,先做一下总结,它初始值为0,它的取值有三个:

  1. REINTERRUPT:1 // 表示线程从等待退出后需要弥补一下中断
  2. THROW_IE :-1 // 表示线程从等待退出后需要抛出中断
  3. 0:没有中断

😃 说的啥意思啊????

cpp 复制代码
/* Mode meaning to reinterrupt on exit from wait */
        private static final int REINTERRUPT =  1;
        
/* Mode meaning to throw InterruptedException on exit from wait */
        private static final int THROW_IE    = -1;

transferAfterCancelledWait

为了回答上述问题,我们可以接着看transferAfterCancelledWait方法,首选,能进入transferAfterCancelledWait这个方法,说明前面的Thread.interrupted()的判断为true,表示发生了中断,但发生中断后,如何判断中断发生在signal之前、还是之后呢?

如果是在signal之前发生的中断,那么肯定还没进入同步队列。那么我们是否可以通过判断同步队列是否存在该节点来达到目的呢?

我觉得可以,不过这种方法复杂度得O(n)吧~而源码中通过compareAndSetWaitStatus方法(CAS操作)试图将CONDITION状态置为0,如果成功,说明是在signal之前发生的中断(signal之后状态已经被修改了)

cpp 复制代码
 /**
     * Transfers node, if necessary, to sync queue after a cancelled wait.
     * Returns true if thread was cancelled before being signalled.
     *
     * @param node the node
     * @return true if cancelled before the node was signalled
     */
    final boolean transferAfterCancelledWait(Node node) {
        // 如果能通过CAS成功将等待状态置为0, 说明中断在signal之前
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            // 进入到同步队列中
            enq(node);
            return true;
        }
        /*
         * If we lost out to a signal(), then we can't proceed
         * until it finishes its enq().  Cancelling during an
         * incomplete transfer is both rare and transient, so just
         * spin.
         */
        // 执行到这里说明上面的CAS不成功
        // 说明中断发生在signal之后
        while (!isOnSyncQueue(node))
            // 线程让步
            Thread.yield();
        return false;
    }

既然CAS不成功,说明中断发生在signal之后,但我还是得 while (!isOnSyncQueue(node))再判断一下,前面已经说了,signal发生时,不是立刻就到达同步队列的,如果同步队列没有,Thread.yield()这里可以理解为先让一下步,缓一缓,等节点到了同步队列,我再返回。


await

transferAfterCancelledWait方法分析完毕,再回到await方法下面这一行代码,此时interruptMode 就不会为0了,即跳出while循环了。

cpp 复制代码
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

此时继续往下,执行这部分代码,当interruptMode为THROW_IE,表示signal之前发生的中断,此时在中断报告中抛出了异常。如果为REINTERRUPT,我们在报告中进行自我中断。这一点和我们分析acquire的时候有点儿像。

究其根本原因都是Thread.interrupted()并不会真的中断,只是进行判断、返回结果并清除中断标识。真正是否中断交由顶层接口进行操作。

cpp 复制代码
 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
     interruptMode = REINTERRUPT;
 if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
  if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);
cpp 复制代码
 private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

总结

本文主要分析await后挂起的线程醒来时,接下来的运行流程。由于醒来时,程序并不知道原因所以需要进行判断。

而醒来的原因又可以分为signal自然唤醒、中断(被迫唤醒)。其中,中断导致的唤醒,又可以分为signal前中断和signal后中断,针对这些情况,本文都做了分析。

(完结)


相关推荐
愤怒的代码2 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰2 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包18 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
夜半被帅醒24 分钟前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql
万亿少女的梦16830 分钟前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
醒了就刷牙1 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
m0_748233641 小时前
SQL数组常用函数记录(Map篇)
java·数据库·sql
编程爱好者熊浪2 小时前
JAVA HTTP压缩数据
java
吴冰_hogan2 小时前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
白宇横流学长3 小时前
基于java出租车计价器设计与实现【源码+文档+部署讲解】
java·开发语言