一文聊透 Netty 核心引擎 Reactor 的运转架构

打印 上一主题 下一主题

主题 841|帖子 841|积分 2523

本系列Netty源码解析文章基于 4.1.56.Final版本

本文笔者来为大家介绍下Netty的核心引擎Reactor的运转架构,希望通过本文的介绍能够让大家对Reactor是如何驱动着整个Netty框架的运转有一个全面的认识。也为我们后续进一步介绍Netty关于处理网络请求的整个生命周期的相关内容做一个前置知识的铺垫,方便大家后续理解。
那么在开始本文正式的内容之前,笔者先来带着大家回顾下前边文章介绍的关于Netty整个框架如何搭建的相关内容,没有看过笔者前边几篇文章的读者朋友也没关系,这些并不会影响到本文的阅读,只不过涉及到相关细节的部分,大家可以在回看下。
前文回顾

《聊聊Netty那些事儿之Reactor在Netty中的实现(创建篇)》一文中,我们介绍了Netty服务端的核心引擎主从Reactor线程组的创建过程以及相关核心组件里的重要属性。在这个过程中,我们还提到了Netty对各种细节进行的优化,比如针对JDK NIO 原生Selector做的一些优化,展现了Netty对性能极致的追求。最终我们创建出了如下结构的Reactor。

在上篇文章《详细图解Netty Reactor启动全流程》中,我们完整地介绍了Netty服务端启动的整个流程,并介绍了在启动过程中涉及到的ServerBootstrap相关的属性以及配置方式。用于接收连接的服务端NioServerSocketChannel的创建和初始化过程以及其类的继承结构。其中重点介绍了NioServerSocketChannel向Reactor的注册过程以及Reactor线程的启动时机和pipeline的初始化时机。最后介绍了NioServerSocketChannel绑定端口地址的整个流程。在这个过程中我们了解了Netty的这些核心组件是如何串联起来的。
当Netty启动完毕后,我们得到了如下的框架结构:

主Reactor线程组中管理的是NioServerSocketChannel用于接收客户端连接,并在自己的pipeline中的ServerBootstrapAcceptor里初始化接收到的客户端连接,随后会将初始化好的客户端连接注册到从Reactor线程组中。
从Reactor线程组主要负责监听处理注册其上的所有客户端连接的IO就绪事件。
其中一个Channel只能分配给一个固定的Reactor。一个Reactor负责处理多个Channel上的IO就绪事件,这样可以将服务端承载的全量客户端连接分摊到多个Reactor中处理,同时也能保证Channel上IO处理的线程安全性。Reactor与Channel之间的对应关系如下图所示:

以上内容就是对笔者前边几篇文章的相关内容回顾,大家能回忆起来更好,回忆不起来也没关系,一点也不影响大家理解本文的内容。如果对相关细节感兴趣的同学,可以在阅读完本文之后,在去回看下。
我们言归正传,正式开始本文的内容,笔者接下来会为大家介绍这些核心组件是如何相互配合从而驱动着整个Netty Reactor框架运转的。
当Netty Reactor框架启动完毕后,接下来第一件事情也是最重要的事情就是如何来高效的接收客户端的连接。
那么在探讨Netty服务端如何接收连接之前,我们需要弄清楚Reactor线程的运行机制,它是如何监听并处理Channel上的IO就绪事件的。
本文相当于是后续我们介绍Reactor线程监听处理ACCEPT事件,Read事件,Write事件的前置篇,本文专注于讲述Reactor线程的整个运行框架。理解了本文的内容,对理解后面Reactor线程如何处理IO事件会大有帮助。
我们在Netty框架的创建阶段和启动阶段无数次的提到了Reactor线程,那么在本文要介绍的运行阶段就该这个Reactor线程来大显神威了。
经过前边文章的介绍,我们了解到Netty中的Reactor线程主要干三件事情:

  • 轮询注册在Reactor上的所有Channel感兴趣的IO就绪事件。
  • 处理Channel上的IO就绪事件。
  • 执行Netty中的异步任务。
正是这三个部分组成了Reactor的运行框架,那么我们现在来看下这个运行框架具体是怎么运转的~~
Reactor线程的整个运行框架

大家还记不记得笔者在《聊聊Netty那些事儿之从内核角度看IO模型》一文中提到的,IO模型的演变是围绕着"如何用尽可能少的线程去管理尽可能多的连接"这一主题进行的。
Netty的IO模型是通过JDK NIO Selector实现的IO多路复用模型,而Netty的IO线程模型为主从Reactor线程模型。
根据《聊聊Netty那些事儿之从内核角度看IO模型》一文中介绍的IO多路复用模型我们很容易就能理解到Netty会使用一个用户态的Reactor线程去不断的通过Selector在内核态去轮训Channel上的IO就绪事件。
说白了Reactor线程其实执行的就是一个死循环,在死循环中不断的通过Selector去轮训IO就绪事件,如果发生IO就绪事件则从Selector系统调用中返回并处理IO就绪事件,如果没有发生IO就绪事件则一直阻塞在Selector系统调用上,直到满足Selector唤醒条件。
以下三个条件中只要满足任意一个条件,Reactor线程就会被从Selector上唤醒:

  • 当Selector轮询到有IO活跃事件发生时。
  • 当Reactor线程需要执行的定时任务到达任务执行时间deadline时。
  • 当有异步任务提交给Reactor时,Reactor线程需要从Selector上被唤醒,这样才能及时的去执行异步任务。
这里可以看出Netty对Reactor线程的压榨还是比较狠的,反正现在也没有IO就绪事件需要去处理,不能让Reactor线程在这里白白等着,要立即唤醒它,转去处理提交过来的异步任务以及定时任务。Reactor线程堪称996典范一刻不停歇地运作着。

在了解了Reactor线程的大概运行框架后,我们接下来就到源码中去看下它的核心运转框架是如何实现出来的。
由于这块源码比较庞大繁杂,所以笔者先把它的运行框架提取出来,方便大家整体的理解整个运行过程的全貌。

上图所展示的就是Reactor整个工作体系的全貌,主要分为如下几个重要的工作模块:

  • Reactor线程在Selector上阻塞获取IO就绪事件。在这个模块中首先会去检查当前是否有异步任务需要执行,如果有异步需要执行,那么不管当前有没有IO就绪事件都不能阻塞在Selector上,随后会去非阻塞的轮询一下Selector上是否有IO就绪事件,如果有,正好可以和异步任务一起执行。优先处理IO就绪事件,在执行异步任务。
  • 如果当前没有异步任务需要执行,那么Reactor线程会接着查看是否有定时任务需要执行,如果有则在Selector上阻塞直到定时任务的到期时间deadline,或者满足其他唤醒条件被唤醒。如果没有定时任务需要执行,Reactor线程则会在Selector上一直阻塞直到满足唤醒条件。
  • 当Reactor线程满足唤醒条件被唤醒后,首先会去判断当前是因为有IO就绪事件被唤醒还是因为有异步任务需要执行被唤醒或者是两者都有。随后Reactor线程就会去处理IO就绪事件和执行异步任务。
  • 最后Reactor线程返回循环起点不断的重复上述三个步骤。
以上就是Reactor线程运行的整个核心逻辑,下面是笔者根据上述核心逻辑,将Reactor的整体代码设计框架提取出来,大家可以结合上边的Reactor工作流程图,从总体上先感受下整个源码实现框架,能够把Reactor的核心处理步骤和代码中相应的处理模块对应起来即可,这里不需要读懂每一行代码,要以逻辑处理模块为单位理解。后面笔者会将这些一个一个的逻辑处理模块在单独拎出来为大家详细介绍。
  1.   @Override
  2.     protected void run() {
  3.         //记录轮询次数 用于解决JDK epoll的空轮训bug
  4.         int selectCnt = 0;
  5.         for (;;) {
  6.             try {
  7.                 //轮询结果
  8.                 int strategy;
  9.                 try {
  10.                     //根据轮询策略获取轮询结果 这里的hasTasks()主要检查的是普通队列和尾部队列中是否有异步任务等待执行
  11.                     strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
  12.                     switch (strategy) {
  13.                     case SelectStrategy.CONTINUE:
  14.                         continue;
  15.                     case SelectStrategy.BUSY_WAIT:
  16.                         // NIO不支持自旋(BUSY_WAIT)
  17.                     case SelectStrategy.SELECT:
  18.                       核心逻辑是有任务需要执行,则Reactor线程立马执行异步任务,如果没有异步任务执行,则进行轮询IO事件
  19.                     default:
  20.                     }
  21.                 } catch (IOException e) {
  22.                        ................省略...............
  23.                 }
  24.                 执行到这里说明满足了唤醒条件,Reactor线程从selector上被唤醒开始处理IO就绪事件和执行异步任务
  25.                 /**
  26.                  * Reactor线程需要保证及时的执行异步任务,只要有异步任务提交,就需要退出轮询。
  27.                  * 有IO事件就优先处理IO事件,然后处理异步任务
  28.                  * */
  29.                 selectCnt++;
  30.                 //主要用于从IO就绪的SelectedKeys集合中剔除已经失效的selectKey
  31.                 needsToSelectAgain = false;
  32.                 //调整Reactor线程执行IO事件和执行异步任务的CPU时间比例 默认50,表示执行IO事件和异步任务的时间比例是一比一
  33.                 final int ioRatio = this.ioRatio;
  34.             
  35.                这里主要处理IO就绪事件,以及执行异步任务
  36.                需要优先处理IO就绪事件,然后根据ioRatio设置的处理IO事件CPU用时与异步任务CPU用时比例,
  37.                来决定执行多长时间的异步任务
  38.                 //判断是否触发JDK Epoll BUG 触发空轮询
  39.                 if (ranTasks || strategy > 0) {
  40.                     if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
  41.                         logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
  42.                                 selectCnt - 1, selector);
  43.                     }
  44.                     selectCnt = 0;
  45.                 } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
  46.                     //既没有IO就绪事件,也没有异步任务,Reactor线程从Selector上被异常唤醒 触发JDK Epoll空轮训BUG
  47.                     //重新构建Selector,selectCnt归零
  48.                     selectCnt = 0;
  49.                 }
  50.             } catch (CancelledKeyException e) {
  51.                 ................省略...............
  52.             } catch (Error e) {
  53.                 ................省略...............
  54.             } catch (Throwable t) {
  55.               ................省略...............
  56.             } finally {
  57.               ................省略...............
  58.             }
  59.         }
  60.     }
复制代码
从上面提取出来的Reactor的源码实现框架中,我们可以看出Reactor线程主要做了下面几个事情:

  • 通过JDK NIO Selector轮询注册在Reactor上的所有Channel感兴趣的IO事件。对于NioServerSocketChannel来说因为它主要负责接收客户端连接所以监听的是OP_ACCEPT事件,对于客户端NioSocketChannel来说因为它主要负责处理连接上的读写事件所以监听的是OP_READ和OP_WRITE事件。
这里需要注意的是netty只会自动注册OP_READ事件,而OP_WRITE事件是在当Socket写入缓冲区以满无法继续写入发送数据时由用户自己注册。

  • 如果有异步任务需要执行,则立马停止轮询操作,转去执行异步任务。这里分为两种情况:

    • 既有IO就绪事件发生,也有异步任务需要执行。则优先处理IO就绪事件,然后根据ioRatio设置的执行时间比例决定执行多长时间的异步任务。这里Reactor线程需要控制异步任务的执行时间,因为Reactor线程的核心是处理IO就绪事件,不能因为异步任务的执行而耽误了最重要的事情。
    • 没有IO就绪事件发生,但是有异步任务或者定时任务到期需要执行。则只执行异步任务,尽可能的去压榨Reactor线程。没有IO就绪事件发生也不能闲着。
    这里第二种情况下只会执行64个异步任务,目的是为了防止过度执行异步任务,耽误了最重要的事情轮询IO事件。

  • 在最后Netty会判断本次Reactor线程的唤醒是否是由于触发了JDK epoll 空轮询 BUG导致的,如果触发了该BUG,则重建Selector。绕过JDK BUG,达到解决问题的目的。
正常情况下Reactor线程从Selector中被唤醒有两种情况:

  • 轮询到有IO就绪事件发生。
  • 有异步任务或者定时任务需要执行。
    JDK epoll 空轮询 BUG会在上述两种情况都没有发生的时候,Reactor线程会意外的从Selector中被唤醒,导致CPU空转。
JDK epoll 空轮询 BUG:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6670302
好了,Reactor线程的总体运行结构框架我们现在已经了解了,下面我们来深入到这些核心处理模块中来各个击破它们~~
1. Reactor线程轮询IO就绪事件

《聊聊Netty那些事儿之Reactor在Netty中的实现(创建篇)》一文中,笔者在讲述主从Reactor线程组NioEventLoopGroup的创建过程的时候,提到一个构造器参数SelectStrategyFactory 。
  1.    public NioEventLoopGroup(
  2.             int nThreads, Executor executor, final SelectorProvider selectorProvider) {
  3.         this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
  4.     }
  5.   public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
  6.                              final SelectStrategyFactory selectStrategyFactory) {
  7.         super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
  8.     }
复制代码
Reactor线程最重要的一件事情就是轮询IO就绪事件,SelectStrategyFactory 就是用于指定轮询策略的,默认实现为DefaultSelectStrategyFactory.INSTANCE。
而在Reactor线程开启轮询的一开始,就是用这个selectStrategy 去计算一个轮询策略strategy ,后续会根据这个strategy 进行不同的逻辑处理。
  1.   @Override
  2.     protected void run() {
  3.         //记录轮询次数 用于解决JDK epoll的空轮训bug
  4.         int selectCnt = 0;
  5.         for (;;) {
  6.             try {
  7.                 //轮询结果
  8.                 int strategy;
  9.                 try {
  10.                     //根据轮询策略获取轮询结果 这里的hasTasks()主要检查的是普通队列和尾部队列中是否有异步任务等待执行
  11.                     strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
  12.                     switch (strategy) {
  13.                     case SelectStrategy.CONTINUE:
  14.                         continue;
  15.                     case SelectStrategy.BUSY_WAIT:
  16.                         // NIO不支持自旋(BUSY_WAIT)
  17.                     case SelectStrategy.SELECT:
  18.                       核心逻辑是有任务需要执行,则Reactor线程立马执行异步任务,如果没有异步任务执行,则进行轮询IO事件
  19.                     default:
  20.                     }
  21.                 } catch (IOException e) {
  22.                        ................省略...............
  23.                 }
  24.                 ................省略...............
  25. }
复制代码
下面我们来看这个轮询策略strategy 具体的计算逻辑是什么样的?
1.1 轮询策略

  1. public interface SelectStrategy {
  2.     /**
  3.      * Indicates a blocking select should follow.
  4.      */
  5.     int SELECT = -1;
  6.     /**
  7.      * Indicates the IO loop should be retried, no blocking select to follow directly.
  8.      */
  9.     int CONTINUE = -2;
  10.     /**
  11.      * Indicates the IO loop to poll for new events without blocking.
  12.      */
  13.     int BUSY_WAIT = -3;
  14.     int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception;
  15. }
复制代码
我们首先来看下Netty中定义的这三种轮询策略:

  • SelectStrategy.SELECT:此时没有任何异步任务需要执行,Reactor线程可以安心的阻塞在Selector上等待IO就绪事件的来临。
  • SelectStrategy.CONTINUE:重新开启一轮IO轮询。
  • SelectStrategy.BUSY_WAIT: Reactor线程进行自旋轮询,由于NIO 不支持自旋操作,所以这里直接跳到SelectStrategy.SELECT策略。
下面我们来看下轮询策略的计算逻辑calculateStrategy :
  1. final class DefaultSelectStrategy implements SelectStrategy {
  2.     static final SelectStrategy INSTANCE = new DefaultSelectStrategy();
  3.     private DefaultSelectStrategy() { }
  4.     @Override
  5.     public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
  6.         /**
  7.          * Reactor线程要保证及时的执行异步任务
  8.          * 1:如果有异步任务等待执行,则马上执行selectNow()非阻塞轮询一次IO就绪事件
  9.          * 2:没有异步任务,则跳到switch select分支
  10.          * */
  11.         return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
  12.     }
  13. }
复制代码

  • 在Reactor线程的轮询工作开始之前,需要首先判断下当前是否有异步任务需要执行。判断依据就是查看Reactor中的异步任务队列taskQueue和用于统计信息任务用的尾部队列tailTask是否有异步任务。
  1.     @Override
  2.     protected boolean hasTasks() {
  3.         return super.hasTasks() || !tailTasks.isEmpty();
  4.     }
  5.    protected boolean hasTasks() {
  6.         assert inEventLoop();
  7.         return !taskQueue.isEmpty();
  8.     }
复制代码

  • 如果Reactor中有异步任务需要执行,那么Reactor线程需要立即执行,不能阻塞在Selector上。在返回前需要再顺带调用selectNow()非阻塞查看一下当前是否有IO就绪事件发生。如果有,那么正好可以和异步任务一起被处理,如果没有,则及时地处理异步任务。
这里Netty要表达的语义是:首先Reactor线程需要优先保证IO就绪事件的处理,然后在保证异步任务的及时执行。如果当前没有IO就绪事件但是有异步任务需要执行时,Reactor线程就要去及时执行异步任务而不是继续阻塞在Selector上等待IO就绪事件。
  1.    private final IntSupplier selectNowSupplier = new IntSupplier() {
  2.         @Override
  3.         public int get() throws Exception {
  4.             return selectNow();
  5.         }
  6.     };
  7.    int selectNow() throws IOException {
  8.         //非阻塞
  9.         return selector.selectNow();
  10.     }
复制代码

  • 如果当前Reactor线程没有异步任务需要执行,那么calculateStrategy 方法直接返回SelectStrategy.SELECT也就是SelectStrategy接口中定义的常量-1。当calculateStrategy 方法通过selectNow()返回非零数值时,表示此时有IO就绪的Channel,返回的数值表示有多少个IO就绪的Channel。
  1.   @Override
  2.     protected void run() {
  3.         //记录轮询次数 用于解决JDK epoll的空轮训bug
  4.         int selectCnt = 0;
  5.         for (;;) {
  6.             try {
  7.                 //轮询结果
  8.                 int strategy;
  9.                 try {
  10.                     //根据轮询策略获取轮询结果 这里的hasTasks()主要检查的是普通队列和尾部队列中是否有异步任务等待执行
  11.                     strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
  12.                     switch (strategy) {
  13.                     case SelectStrategy.CONTINUE:
  14.                         continue;
  15.                     case SelectStrategy.BUSY_WAIT:
  16.                         // NIO不支持自旋(BUSY_WAIT)
  17.                     case SelectStrategy.SELECT:
  18.                       核心逻辑是有任务需要执行,则Reactor线程立马执行异步任务,如果没有异步任务执行,则进行轮询IO事件
  19.                     default:
  20.                     }
  21.                 } catch (IOException e) {
  22.                        ................省略...............
  23.                 }
  24.                 ................处理IO就绪事件以及执行异步任务...............
  25. }
复制代码
从默认的轮询策略我们可以看出selectStrategy.calculateStrategy只会返回三种情况:


  • 返回 -1: switch逻辑分支进入SelectStrategy.SELECT分支,表示此时Reactor中没有异步任务需要执行,Reactor线程可以安心的阻塞在Selector上等待IO就绪事件发生。
  • 返回 0: switch逻辑分支进入default分支,表示此时Reactor中没有IO就绪事件但是有异步任务需要执行,流程通过default分支直接进入了处理异步任务的逻辑部分。
  • 返回 > 0:switch逻辑分支进入default分支,表示此时Reactor中既有IO就绪事件发生也有异步任务需要执行,流程通过default分支直接进入了处理IO就绪事件和执行异步任务逻辑部分。
现在Reactor的流程处理逻辑走向我们清楚了,那么接下来我们把重点放在SelectStrategy.SELECT分支中的轮询逻辑上。这块是Reactor监听IO就绪事件的核心。
1.2 轮询逻辑

  1.                     case SelectStrategy.SELECT:
  2.                         //当前没有异步任务执行,Reactor线程可以放心的阻塞等待IO就绪事件
  3.                         //从定时任务队列中取出即将快要执行的定时任务deadline
  4.                         long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
  5.                         if (curDeadlineNanos == -1L) {
  6.                             // -1代表当前定时任务队列中没有定时任务
  7.                             curDeadlineNanos = NONE; // nothing on the calendar
  8.                         }
  9.                         //最早执行定时任务的deadline作为 select的阻塞时间,意思是到了定时任务的执行时间
  10.                         //不管有无IO就绪事件,必须唤醒selector,从而使reactor线程执行定时任务
  11.                         nextWakeupNanos.set(curDeadlineNanos);
  12.                         try {
  13.                             if (!hasTasks()) {
  14.                                 //再次检查普通任务队列中是否有异步任务
  15.                                 //没有的话开始select阻塞轮询IO就绪事件
  16.                                 strategy = select(curDeadlineNanos);
  17.                             }
  18.                         } finally {
  19.                             // 执行到这里说明Reactor已经从Selector上被唤醒了
  20.                             // 设置Reactor的状态为苏醒状态AWAKE
  21.                             // lazySet优化不必要的volatile操作,不使用内存屏障,不保证写操作的可见性(单线程不需要保证)
  22.                             nextWakeupNanos.lazySet(AWAKE);
  23.                         }
复制代码
流程走到这里,说明现在Reactor上没有任何事情可做,可以安心的阻塞在Selector上等待IO就绪事件到来。
那么Reactor线程到底应该在Selector上阻塞多久呢??
在回答这个问题之前,我们在回顾下《聊聊Netty那些事儿之Reactor在Netty中的实现(创建篇)》一文中在讲述Reactor的创建时提到,Reactor线程除了要轮询Channel上的IO就绪事件,以及处理IO就绪事件外,还有一个任务就是负责执行Netty框架中的异步任务。

而Netty框架中的异步任务分为三类:

  • 存放在普通任务队列taskQueue中的普通异步任务。
  • 存放在尾部队列tailTasks 中的用于执行统计任务等收尾动作的尾部任务。
  • 还有一种就是这里即将提到的定时任务。存放在Reactor中的定时任务队列scheduledTaskQueue中。
从ReactorNioEventLoop类中的继承结构我们也可以看出,Reactor具备执行定时任务的能力。

既然Reactor需要执行定时任务,那么它就不能一直阻塞在Selector上无限等待IO就绪事件。
那么我们回到本小节一开始提到的问题上,为了保证Reactor能够及时地执行定时任务,Reactor线程需要在即将要执行的的第一个定时任务deadline到达之前被唤醒。
所以在Reactor线程开始轮询IO就绪事件之前,我们需要首先计算出来Reactor线程在Selector上的阻塞超时时间。
1.2.1 Reactor的轮询超时时间

首先我们需要从Reactor的定时任务队列scheduledTaskQueue 中取出即将快要执行的定时任务deadline。将这个deadline作为Reactor线程在Selector上轮询的超时时间。这样可以保证在定时任务即将要执行时,Reactor现在可以及时的从Selector上被唤醒。
  1.     private static final long AWAKE = -1L;
  2.     private static final long NONE = Long.MAX_VALUE;
  3.     // nextWakeupNanos is:
  4.     //    AWAKE            when EL is awake
  5.     //    NONE             when EL is waiting with no wakeup scheduled
  6.     //    other value T    when EL is waiting with wakeup scheduled at time T
  7.     private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE);
  8.       long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
  9.       if (curDeadlineNanos == -1L) {
  10.             // -1代表当前定时任务队列中没有定时任务
  11.             curDeadlineNanos = NONE; // nothing on the calendar
  12.       }
  13.       nextWakeupNanos.set(curDeadlineNanos);
复制代码
[code]public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {    PriorityQueue scheduledTask = peekScheduledTask();        return scheduledTask != null ? scheduledTask.deadlineNanos() : -1;    }    final ScheduledFutureTask peekScheduledTask() {        Queue

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

涛声依旧在

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表