【Q-01】与 parentChannel 相绑定的线程是在执行什么任务时创建的?请谈一下你的认识。
【RA】与 parentChannel 相绑定的任务是在执行 channel 注册到 selector 的任务时创建的。
【Q-02】Netty 中像端口绑定等都是以任务的形式出现的,但源码中出现了任务封装为新的
任务,任务套任务的情况,这是为什么?请谈一下你的认识。
【RA】Netty 中像端口绑定等都是以任务的形式出现的,但源码中出现了任务封装为新的任
务,任务套任务的情况,这些不同等级的任务,都会完成不同的功能,是责任链设计模式的
体现。
【Q-03】由 EventLoop 绑定线程来完成的任务有几种类型?分别存放在哪里?请谈一下你的
认识。
【RA】由 EventLoop 绑定线程来完成的任务有三种类型:
定时任务:该类型任务会存放到定时任务队列 scheduledTaskQueue 中。
普通任务:该类型任务会存放到任务队列 taskQueue 中。我们在当前解析的 Netty 源码
中见到的任务,都是这类任务。当然,定时任务最终也会从定时任务队列中逐个取出,
然后放入到 taskQueue 中来执行。
收尾任务:该类型任务会存放到任务队列 tailTasks 中。其定义方式与普通任务定义方式
相同,只不过由于其主要用于完成一些收尾工作,所以被添加到了 tailTasks 队列中了。
【Q-04】NioEventLoop 中有一个成员变量 wakenUp,其是一个原子布尔类型。这个变量的值
对于 selector 选择源码的阅读很重要。它的值代表什么意义?请谈一下你的看法。
【RA】NioEventLoop 中有一个成员变量 wakenUp,其是一个原子布尔类型。其取值意义为:
true:表示当前 eventLoop 所绑定的线程处于非阻塞状态,即唤醒状态
false:表示当前 eventLoop 所绑定的线程即将被阻塞
【Q-05】在 selector 中有一个方法 wakeup(),其意义对于 selector 选择源码的阅读很重要。
但这个方法的意义仅从其方法名上来理解,很容易产生误解。那么这个方法表示什么意思呢?
请谈一下你的看法。
【RA】该方法会使选择操作立即结束,保存选择结果到 selector。而选择操作的结束,会使
其调用者线程被唤醒
【Q-06】在 selector 中有一个带参数的 select()方法,请谈一下你对这个方法的认识。
【RA】在selector中有一个带参数的select()方法,这个方法的功能是阻塞式选择就绪channel。
即其调用者方法所在线程在调用了这个方法后会发生阻塞。而唤醒阻塞的条件有五个,哪个
先到哪个其作用:
有 channel 就绪被选择
wakeup()方法被调用
当前线程的阻塞被打断
给定的阻塞时间到了
当空轮询次数较多时会引发 CPU 占用率飙升,为了保护系统,也会结束 select()方法,
唤醒阻塞
【Q-07】在 selector 中有一个 selectNow()方法,请谈一下你对这个方法的认识。
【RA】在 selector 中有一个 selectNow()方法,这个方法的功能是非阻塞式选择就绪 channel。
这个方法只会对注册的 channel 遍历一次,在遍历过程中如果发现就绪的 channel,则直接
返回,没有发现,则返回 0。
【Q-08】ScheduledFutureTask 中有一个常量 START_TIME,这个常量的意义是什么?请谈一
下你的认识。
【RA】START_TIME 是 static final long 类型的常量,static final 类型常量是在当前类加载时完
成的初始化,即该常量是类级别的常量,对于所有定时任务来说,这个 START_TIME 常量值
都是相同的。所以该常量表示意义就是当前定时任务类的加载时间点,而所有定时任务要推
迟执行的时间都是基于这具时间点的时长。
第 4 次直播课
【Q-01】NioEventLoop 中的 run()方法用于进行就绪 channel 的选择、处理就绪 channel 的 IO
及完成任务队列中的任务。其中有一个变量 ioRatio 较为重要,其有什么作用?请谈一下你
的认识。
【RA】NioEventLoop 中的 run()方法中有一个变量 ioRatio,其表示处理就绪 channel 的 IO 操
作与处理任务队列任务用时的占比。其值不大于 100,表示占 100 的比例。例如 IO 操作用
时 10 秒,ioRatio 的值为 20,那么处理任务队列任务用时为 10 秒 * (100 - 20)/20 = 10 秒
* 5 = 50 秒。
【Q-02】NioEventLoop 对 selectedKeys 进行了优化,其是如何优化的?为什么这样做?请谈
一下你的认识。
【RA】Netty 对 SelectedKeys 的优化简单来说就是,将原来的简单的 Set 集合封装为了一个
新的类,这个类中维护着一个数组,用于存放原来存放在 Set 集合中的元素。我们知道,在
数组长度不变的情况下,数组的执行效率要高于 Set、List 等集合。因为数组元素是顺序存
储的,而集合元素是链式存储的。
【Q-03】NioEventLoop 在对就绪 channel 进行处理时出现了对 readyOps 为 0 的处理。readyOps
表示没有就绪 channel,为什么对就绪 channel 的处理中会出现 readyOps 为 0 的情况?请谈
一下你的认识。
【RA】NioEventLoop 在对就绪 channel 进行处理时出现了对 readyOps 为 0 的处理。readyOps
为 0 的结果是执行非阻塞选择 selectNow()时可能返回的值。selectNow()结束表示选择结束,
选择结束就需要对选择结果进行处理,所以在对就绪 channel 进行处理时出现了对 readyOps
为 0 的处理。
【Q-04】NioEventLoop 在对就绪 channel 进行处理时出现了对 readyOps 为 0 的处理。处理方
式是执行了一次 unsafe.read()。readyOps 表示没有就绪 channel,既然没有就绪,那么其要
读什么?为什么要这样操作?请谈一下你的认识。
【RA】NioEventLoop 在对就绪 channel 进行处理时出现了对 readyOps 为 0 的处理。处理方
式是执行了一次 unsafe.read()。readyOps 表示没有就绪 channel,既然没有就绪,这里的读
操作仅仅就是一次“空读”,仅执行了一次读操作流程。这样做是为了解决NIO中的一个Bug: 开课吧 Reythor 雷课程面试题暨知识点总结
讲师:Reythor 雷
6
由于长时间没有就绪 channel 而导致的高速轮询,高速轮询就可能导致 CPU 使用率飙升从而
使系统崩溃。仅执行一次读操作流程,就会使“高速轮询”降速下来。
【Q-05】Netty 中的定时任务从定义到被执行,都经历了些什么?请谈一下你的认识。
【RA】Netty 中的定时任务被定义好后,会直接被放入到定时任务队列,然后在定时任务对
应的 channel 被选择并完成响应的 IO 后,会将定时任务从定时任务队列取出并放入到任务
队列中,有 channel 绑定的 EventLoop 绑定的线程完成执行。
【Q-05】Netty 中的定时任务从定义到被执行,都经历了些什么?请谈一下你的认识。
【RA】Netty 中的定时任务被定义好后,会直接被放入到定时任务队列,然后在定时任务对
应的 channel 被选择并完成响应的 IO 后,会将定时任务从定时任务队列取出并放入到任务
队列中,有 channel 绑定的 EventLoop 绑定的线程完成执行。
【Q-07】Netty 中在从定时任务队列中查找开始执行时间小于指定时间的定时任务时,为什
么仅从队列中 peek()一个任务判断,而不是遍历整个定时任务队列?请谈一下你的认识。
【RA】Netty 中在从定时任务队列中查找开始执行时间小于指定时间的定时任务时,仅从队
列中 peek()一个任务判断,若满足小于等于指定时间的条件,则返回这个定时任务;不满足
则直接返回 null,而不是遍历整个定时任务队列。因为定时任务队列中的任务时按照任务开
始执行时间,由小到大排列好的。若 peek()出的第一个不能满足条件,后面的任务更不会满
足。所以不用对定时任务队列进行遍历。
【Q-08】Netty 在执行任务时其所使用的时间会与对就绪 channel 的 IO 执行时间存在比例关
系。Netty 对于这个任务执行时间是精确控制的吗?请谈一下你的认识。
【RA】Netty 在执行任务时其所使用的时间会与对就绪 channel 的 IO 执行时间存在比例关系。
Netty 对于这个任务执行时间不是精确控制的。其会每执行 64 个任务判断一次是否超时。这
样的话就可能在第 64 个任务执行完毕时刚好不超时,而在执行第 65 个任务时超时。但由于
没有进行超时判断,所以后面 64 个都会在超时的情况下执行完毕。此时在判断超时,发现
已经超时,这是才会结束对任务队列的执行。
之所以这样设计,是因为在这个过程中的当前时间计算都是使用的 System.nanoTime(),
这个时间获取比较消耗系统资源。为了提高效率,才硬编码为每 64 个任务判断一次超时。
【Q-09】Netty 的收尾任务是在什么时候执行的,是在所有任务队列中的任务执行完毕后才
执行吗?请谈一下你的认识。
【RA】Netty 的收尾任务不是在所有任务队列中的任务执行完毕后才执行的。Netty 对于任
务的执行会有一个超时时间,当超时时间过期时就会执行收尾任务队列中的任务,此时可能
任务队列中的任务还没执行完毕。
【Q-10】Netty Server中端口绑定、parentChannel注册到selector、childChannel注册到selector,
这三个都是以任务的完成的。这三个任务执行的顺序是怎样的?这三个任务的区别于联系是
什么?请谈一下你的认识。
【RA】Netty Server 中这三个任务的执行顺序是:parentChannel 注册到 selector、端口绑定、
childChannel 注册到 selector。
在 Netty Server 启动时调用了 parentChannel 注册到 selector 任务。在完成这个任务时完
成了从 parentEventLoopGroup 中选择出一个 EventLoop,并于 parentChannel 相绑定,完成了
注册任务添加到任务队列,完成了与 EventLoop 相绑定的线程的创建与启动。
在 Netty Server 启动时调用了端口绑定任务。其仅仅完成了绑定任务添加到任务队列。
至于执行,就由一直没有停止执行的、与 parentChannel 相绑定的 EventLoop 相绑定的线程
自动完成。
在 Netty Server 启动后,Netty Server 的连接请求到达后,调用了 childChannel 注册到
selector 任务。在完成这个任务时完成了从 childEventLoopGroup 中选择出一个 EventLoop,
并于 childChannel 相绑定,完成了注册任务添加到任务队列,完成了与 EventLoop 相绑定的
线程的创建与启动。
【Q-11】Netty 中如何添加定时任务?请谈一下你的认识。
【RA】Netty 中要想添加定时任务,只需获取到 channel 的 eventLoop,然后调用 eventLoop
的 schedule()方法即可添加一个 Runnable 定时任务到定时任务队列。
【Q-12】Netty Client 端 Channel 的创建过程与 Netty Server 端 parentChannel 的创建过程相同,
但其初始化过程略有不同。不同主要体现在哪里?请谈一下你的看法。
【RA】parentChannel 在初始化过程中需要将连接处理器注册到 channel 的 pipeline 中。这个
连接处理器用于处理Client端的连接操作,为Client端在Server处生成其对应的childChannel,
并注册到相应的 selector。但 Client 端的 channel 无需注册连接处理器,因为它是连接的发出
者,而非接收者。
【Q-13】Netty Client 在连接指定的 Netty Server 地址之前,首先解析了这个指定的地址。请
简单谈一下你对这个解析过程的认识。
【RA】Netty Client 在连接指定的 Netty Server 地址之前,首先解析了这个指定的地址。这个
解析就是将指定的主机名解析为了 IP,这个解析过程是一个异步过程。
【Q-01】ChannelPipeline 是在什么时候创建的?请从源码角度简要谈一下你的认识。
【RA】ChannelPipeline 是在创建 Channel 时创建的。一个 Channel 对应一个 Pipeline。创建
Channel 时使用的是反射机制,调用了 NioServerSocketChannel 的无参构造器。而该构造器最
终调用了 AbstractChannel 的构造器。ChannelPipeline 就是在 AbstractChannel 的构造器中创
建的。
【Q-02】Netty 中的 ChannelPipeline 是一个比较重要的概念,ChannelPipeline 本质上是个什
么?其又是怎么添加节点的?请简单谈一下你的认识。
【RA】ChannelPipeline 是在创建 Channel 是创建的,其是 Channel 一个很重要的成员。其本
质上是一个双向链表,默认具有头、尾两个节点。除了这两个节点外,其还可以通过
channelPipeline 的 addLast()方法向其中添加处理器节点。每一个处理器最终都会被封装为一
下 channelPipeline 上的节点。
【Q-03】ChannelPipeline 中的处理器的删除过程都做了哪些重要工作?请谈一下你的认识。
【RA】处理器从 ChannelPipeline 中的删除过程主要做了如下几项工作:
从 ChannelPipeline 中查找是否存在该处理器对应的节点。若存在,则进行删除。
由于其删除的是节点,所以,会首先从 ChannelPipeline 中找到该处理器节点,然后从 开课吧 Reythor 雷课程面试题暨知识点总结
讲师:Reythor 雷
8
ChannelPipeline 的双向链表中删除该节点。
最后触发该处理器 handlerRemoved()方法的执行
【Q-04】在添加处理器到 ChannelPipeline 时可以为该处理器指定名称,若没有指定系统会
为其自动生成一个名称。这个自动生成的名称格式是怎样的?请谈一下你的认识。
【RA】在将处理器添加到 ChannelPipeline 中时若没有指定名称,系统会自动为其生成一个
名称,该名称为该处理器类的简单类名后跟一个#,然后是一个数字。从 0 开始尝试。若该
名称在 ChannelPipeline 中存在,则数字加一,直到找到不重复的数字为止。
【Q-05】在 ChannelInitializer 类上为什么需要添加@Sharable?请谈一下你的认识。
【RA】@Sharable 注解添加到一个处理器类上表示该处理器是共享的,可以被多次添加到同
一个 ChannelPipeline 中,也可以被添加到多个 ChannelPipeline 中。
服务端启动类中定义的 ChannelInitializer 实例是在 Server 启动时创建的,然后每过来一
个 Client 连接,就会将这个 ChannelInitializer 实例添加到一个 childChannel 的 pipeline 中。即
一个 ChannelInitializer 处理器实例被添加到了连接到当前 Server 的所有客户端对应的所有
childChannel 的 pipeline 中。这也就是为什么需要在 ChannelInitializer 类上添加@Sharable 注
解的原因。
【Q-06】对于 ChannelInitializer 处理器实例的创建、删除,都与一个 initMap 有成员变量相
关。请谈一下你对这个 initMap 的认识。
【RA】ChannelInitializer 处理器是一个共享处理器,为了减少内存的使用,在其中定义了一
个成员变量 initMap。它是一个 JUC 的 Set 集合,其中存放着所有由该 ChannelInitializer 处理
器实例创建出的节点实例。
需要清楚一点:每个处理器都会通过 new 被创建为为一个节点后才能被添加到 pipeline
中。ChannelInitializer 是一个共享处理器,但通过其 new 出来的节点是多例的,不是共享的。
为了方便管理,在 ChannelInitializer 中维护了一个 JUC 的 Set 集合,集合元素为该处理器创
建出的节点实例。这个 Set 集合就是这个 initMap。
【Q-07】简述在 Server 端 bootstrap 中定义的 ChannelInitializer 处理器的创建、添加时机,
及添加到哪个 channel 的 pipeline 中了?
【RA】在 Server 端 bootstrap 中定义的 ChannelInitializer 处理器的创建、添加时机,及添加
位置如下:
创建时机:在 Server 启动时被创建
添加位置:最终会被添加到 childChannel 的 pipeline 中。因为其是通过 bootstrap 中的
childHandler()完成的初始化
添加时机:每过来一个 Client 连接,就会将该处理器添加到一个 childChannel 的 pipeline
中,但添加的这个处理器实例,都是在 Server 启动时创建的那一个
【Q-08】简述在 Client 端 bootstrap 中定义的 ChannelInitializer 处理器的创建、添加时机,及
添加到哪个 channel 的 pipeline 中了?
【RA】在 Client 端 bootstrap 中定义的 ChannelInitializer 处理器的创建、添加时机,及添加位
置如下:
创建时机:在 Client 启动时被创建
添加位置:被添加到 Channel 的 pipeline 中,Client 端没有 parentChannel 与 childChannel 开课吧 Reythor 雷课程面试题暨知识点总结
讲师:Reythor 雷
9
的区分
添加时机:在 Client 启动时被添加
【Q-09】我们注意到,在 Netty 中,执行完毕 ChannelInitializer 处理器的 initChannel()方法后,
马上就会将这个处理器删除。为什么?请谈一下你的认识。
【RA】ChannelInitializer 是一个比较特殊的处理器。其作用就是将其重写的 initChannel()方法
执行完毕,然后该处理器的历史使命也就完成了,此时就可以将其从 pipeline 中删除了。一
般 initChannel()方法中都是放的一些向 channelPipeline 中添加普通处理器的语句,或一些对
channel 进行初始化的语句
【Q-01】ChannelInboundHandler 中都包含了哪一类的方法?请谈一下你的认识。
【RA】ChannelInboundHandler 中包含了像 channelRead()、channelRegistered()、channelActive()
等回调方法,即由其它事件所触发的发法。
【Q-02】ChannelOutboundHandler 中都包含了哪一类的方法?请谈一下你的认识。
【RA】ChannelOutboundHandler 中包含了像 bind()、connet()、close()等方法,这些方法一般
都是由 Outbound 处理器实例主动调用执行的,而最终是由 channel 的 unsafe 完成的。
【Q-03】简述一下 ChanneHandler 接口。
【RA】ChanneHandler 接口是 ChannelInboundHandler 与 ChannelOutboundHandler 接口的父
接口,其包含两个方法 handlerAdded()与 handlerRemoved()。也就是说,这两个方法是所有
处理器都具有的方法。
【Q-04】ChanneHandlerContext 接口对于 ChannelPipeline 的理解很重要,请简述一下
ChanneHandlerContext 接口。
【RA】ChanneHandlerContext 实例就是一个 ChannelPipeline 节点,是一个双向链表节点,其
可以调用 InboundHandler 的方法,也可以调用 OutboundHandler 的方法,以引来触发下一个
节点相应方法的执行。同时也可以获取到设置到 channel 中的 attr 属性。
【Q-05】Channe 中的 attr 属性是在哪里设置的?
【RA】无论是 Server 还是 Client,它们在启动时会创建并初始化 bootstrap,此时可以调用其
attr()或 childAttr()方法,将指定的 attr 属性初始化到 bootstrap 中。然后在 channel 创建后进
行初始化时会将 bootstrap 中配置的设置信息初始化到 channel 中。这些信息中就包含 attr
属性。
【Q-06】简述一下 ChannePipeline 接口及其重要实现类 DefaultChannelPipeline。
【RA】ChannePipeline 是一个 ChannelHandlers 列表。该接口是继承了 ChannelInboundInvoker、
ChannelOutboundInvoker 接口,说明其可以触发 Inbound 处理器方法,可以调用 Outbound
处理器方法。同时,其也继承了 Iterator 操,说明其是可迭代的。
该接口有一个重要实现类 DefaultChannelPipeline。
DefaultChannelPipeline 类实现了 ChannelPipline 接口中有关 ChannelInboundInvoker 中的 开课吧 Reythor 雷课程面试题暨知识点总结
讲师:Reythor 雷
10
方法,这些方法基本都是调用了抽象节点类 AbstractChannelHandlerContext 的相关静态方法,
去调用 head 节点的相应方法。
DefaultChannelPipeline 类还实现了 ChannelPipline 接口中有关 ChannelOutboundInvoker
中的方法,这些方法基本都是调用 tail 结点的相关方法,完成底层的真正执行。
另外,DefaultChannelPipeline 类还实现了 ChannelPipline 接口中 Iterable 接口的方法
iterator()。其迭代的是一个 map 的 entrySet,这个 map 的 key 为节点名称,而 value 为节点
所封装的处理器实例。
【Q-07】简述一下 ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 处理器
的区别及应用场景。
【RA】若我们使用 ChannelInboundHandlerAdapter,则需要我们自己释放 msg,而使用
SimpleChannelInboundHandler,则系统会自动释放。所以,使用哪个类作为处理器的父类,
关键要看是否有需要释放的消息。
一般情况下,若 channelRead()中从对端接收到的 msg(或其封装实例)需要通过
writeAndFlush() 等 方 法 发 送 给 对 端 , 则 该 msg 不 能 释 放 , 所 以 需 要 使 用
ChannelInboundHandlerAdapter 由我们自行控制 msg 的释放。当然,若根本就不需要从对端
读取数据,则直接使用 ChannelInboundHandlerAdapter。若使用 SimpleChannelInboundHandler
还需要重写 channelRead0()方法。
【Q-08】ChannelPipline 与 ChannelHandlerContext 都具有 fireChannelRead()方法,请简述一
下它们的区别。
【RA】ChannelPipline 中的 fireChannelRead()方法会从 head 节点的 channelRead()方法开始触
发 pipeline 中节点的 channelRead()方法;而 ChannelHandlerContext 中的 fireChannelRead()方
法则是触发当前节点的后面节点的 channelRead()方法。
【Q-09】简述在 pipeline 中的多个处理器中都定义了 channelActive()与 handlerAdded()两个方
法,请简述它们执行的区别。
【RA】在 pipeline 中的多个处理器中的多个 channelActive()方法,只有第一个该方法会执行,
因为 channel 只会被激活一次。而 handlerAdded()方法则不同,所以处理器中的该方法都会
在当前处理器被添加到 pipeline 时被触发执行。
【Q-10】简述消息在 inboundHandler、outboundHandler 中的传递顺序,及发生异常后,异
常信息在 inboundHandler、outboundHandler 中的传递顺序。
【RA】消息在 inboundHandler 中 channelRead()方法中的传递顺序为,从 head 节点开始逐个
向后传递,直到传递给 tail 节点将该消息释放。
消息在outboundHandler中write()方法中的传递顺序为,从tail节点开始逐个向前传递,
直到传递到 head 节点,然后调用 unsafe 的 write()方法完成底层写操作。
若发生异常,异常信息会从当前发生异常的节点开始调用 exceptionCaught()方法,并向
后面节点传递,无论后面节点是 inboundHandler 还是 outboundHandler,最后传递到 tail 节
点的 exceptionCaught()方法,将异常消息释放。
当然,前述的向后传递或向前传递的前提是,必须要在节点方法中调用传递到下一个节
点的方法,否则是无法传递的。