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后中断,针对这些情况,本文都做了分析。

(完结)


相关推荐
PypYCCcccCc4 分钟前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐25 分钟前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦38 分钟前
ThreadLocal 线程变量
java·后端
BillKu1 小时前
Java后端检查空条件查询
java·开发语言
jackson凌1 小时前
【Java学习笔记】String类(重点)
java·笔记·学习
刘白Live1 小时前
【Java】谈一谈浅克隆和深克隆
java
一线大码1 小时前
项目中怎么确定线程池的大小
java·后端
要加油哦~1 小时前
vue · 插槽 | $slots:访问所有命名插槽内容 | 插槽的使用:子组件和父组件如何书写?
java·前端·javascript
crud2 小时前
Spring Boot 3 整合 Swagger:打造现代化 API 文档系统(附完整代码 + 高级配置 + 最佳实践)
java·spring boot·swagger
天天摸鱼的java工程师2 小时前
从被测试小姐姐追着怼到运维小哥点赞:我在项目管理系统的 MySQL 优化实战
java·后端·mysql