解决Netty那些事儿之Reactor在Netty中的实现(创建篇)-下
接上文解决Netty那些事儿之Reactor在Netty中的实现(创建篇)-上
Netty对JDK NIO 原生Selector的优化
首先在NioEventLoop
中有一个Selector优化开关DISABLE_KEY_SET_OPTIMIZATION
,通过系统变量-D io.netty.noKeySetOptimization
指定,默认是开启的,表示需要对JDK NIO原生Selector
进行优化。
如果优化开关DISABLE_KEY_SET_OPTIMIZATION
是关闭的,那么直接返回JDK NIO原生的Selector
。
下面为Netty对JDK NIO原生的Selector
的优化过程:
获取
JDK NIO原生Selector
的抽象实现类sun.nio.ch.SelectorImpl
。JDK NIO原生Selector
的实现均继承于该抽象类。用于判断由SelectorProvider
创建出来的Selector
是否为JDK默认实现
(SelectorProvider
第三种加载方式)。因为SelectorProvider
可以是自定义加载,所以它创建出来的Selector
并不一定是JDK NIO 原生的。
JDK NIO Selector的抽象类sun.nio.ch.SelectorImpl
这里笔者来简单介绍下JDK NIO中的Selector
中这几个字段的含义,我们可以和上篇文章讲到的epoll在内核中的结构做类比,方便大家后续的理解:

Set<SelectionKey> selectedKeys
类似于我们上篇文章讲解Epoll
时提到的就绪队列eventpoll->rdllist
,Selector
这里大家可以理解为Epoll
。Selector
会将自己监听到的IO就绪
的Channel
放到selectedKeys
中。
这里的
SelectionKey
暂且可以理解为Channel
在Selector
中的表示,类比上图中epitem结构
里的epoll_event
,封装IO就绪Socket的信息。其实SelectionKey
里包含的信息不止是Channel
还有很多IO相关的信息。后面我们在详细介绍。
HashSet<SelectionKey> keys:
这里存放的是所有注册到该Selector
上的Channel
。类比epoll中的红黑树结构rb_root
SelectionKey
在Channel
注册到Selector
中后生成。
Set<SelectionKey> publicSelectedKeys
相当于是selectedKeys
的视图,用于向外部线程返回IO就绪
的SelectionKey
。这个集合在外部线程中只能做删除操作不可增加元素
,并且不是线程安全的
。Set<SelectionKey> publicKeys
相当于keys
的不可变视图,用于向外部线程返回所有注册在该Selector
上的SelectionKey
这里需要重点关注
抽象类sun.nio.ch.SelectorImpl
中的selectedKeys
和publicSelectedKeys
这两个字段,注意它们的类型都是HashSet
,一会优化的就是这里!!!!
判断由
SelectorProvider
创建出来的Selector
是否是JDK NIO原生的Selector
实现。因为Netty优化针对的是JDK NIO 原生Selector
。判断标准为sun.nio.ch.SelectorImpl
类是否为SelectorProvider
创建出Selector
的父类。如果不是则直接返回。不在继续下面的优化过程。
通过前面对SelectorProvider
的介绍我们知道,这里通过provider.openSelector()
创建出来的Selector
实现类为KQueueSelectorImpl类
,它继承实现了sun.nio.ch.SelectorImpl
,所以它是JDK NIO 原生的Selector
实现
创建
SelectedSelectionKeySet
通过反射替换掉sun.nio.ch.SelectorImpl类
中selectedKeys
和publicSelectedKeys
的默认HashSet
实现。
为什么要用SelectedSelectionKeySet
替换掉原来的HashSet
呢??
因为这里涉及到对HashSet类型
的sun.nio.ch.SelectorImpl#selectedKeys
集合的两种操作:
插入操作: 通过前边对
sun.nio.ch.SelectorImpl类
中字段的介绍我们知道,在Selector
监听到IO就绪
的SelectionKey
后,会将IO就绪
的SelectionKey
插入sun.nio.ch.SelectorImpl#selectedKeys
集合中,这时Reactor线程
会从java.nio.channels.Selector#select(long)
阻塞调用中返回(类似上篇文章提到的epoll_wait
)。遍历操作:
Reactor线程
返回后,会从Selector
中获取IO就绪
的SelectionKey
集合(也就是sun.nio.ch.SelectorImpl#selectedKeys
),Reactor线程
遍历selectedKeys
,获取IO就绪
的SocketChannel
,并处理SocketChannel
上的IO事件
。
我们都知道HashSet
底层数据结构是一个哈希表
,由于Hash冲突
这种情况的存在,所以导致对哈希表
进行插入
和遍历
操作的性能不如对数组
进行插入
和遍历
操作的性能好。
还有一个重要原因是,数组可以利用CPU缓存的优势来提高遍历的效率。后面笔者会有一篇专门的文章来讲述利用CPU缓存行如何为我们带来性能优势。
所以Netty为了优化对sun.nio.ch.SelectorImpl#selectedKeys
集合的插入,遍历
性能,自己用数组
这种数据结构实现了SelectedSelectionKeySet
,用它来替换原来的HashSet
实现。
【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


SelectedSelectionKeySet
初始化
SelectionKey[] keys
数组大小为1024
,当数组容量不够时,扩容为原来的两倍大小。通过数组尾部指针
size
,在向数组插入元素的时候可以直接定位到插入位置keys[size++]
。操作一步到位,不用像哈希表
那样还需要解决Hash冲突
。对数组的遍历操作也是如丝般顺滑,CPU直接可以在缓存行中遍历读取数组元素无需访问内存。比
HashSet
的迭代器java.util.HashMap.KeyIterator
遍历方式性能不知高到哪里去了。
看到这里不禁感叹,从各种小的细节可以看出Netty对性能的优化简直淋漓尽致,对性能的追求令人发指。细节真的是魔鬼。
Netty通过反射的方式用
SelectedSelectionKeySet
替换掉sun.nio.ch.SelectorImpl#selectedKeys
,sun.nio.ch.SelectorImpl#publicSelectedKeys
这两个集合中原来HashSet
的实现。
反射获取
sun.nio.ch.SelectorImpl
类中selectedKeys
和publicSelectedKeys
。
Java9
版本以上通过sun.misc.Unsafe
设置字段值的方式
通过反射的方式用SelectedSelectionKeySet
替换掉hashSet
实现的sun.nio.ch.SelectorImpl#selectedKeys,sun.nio.ch.SelectorImpl#publicSelectedKeys
。
将与sun.nio.ch.SelectorImpl
类中selectedKeys
和publicSelectedKeys
关联好的Netty优化实现SelectedSelectionKeySet
,设置到io.netty.channel.nio.NioEventLoop#selectedKeys
字段中保存。
后续
Reactor线程
就会直接从io.netty.channel.nio.NioEventLoop#selectedKeys
中获取IO就绪
的SocketChannel
用
SelectorTuple
封装unwrappedSelector
和wrappedSelector
返回给NioEventLoop
构造函数。到此Reactor
中的Selector
就创建完毕了。
所谓的
unwrappedSelector
是指被Netty优化过的JDK NIO原生Selector。所谓的
wrappedSelector
就是用SelectedSelectionKeySetSelector
装饰类将unwrappedSelector
和与sun.nio.ch.SelectorImpl类
关联好的Netty优化实现SelectedSelectionKeySet
封装装饰起来。
wrappedSelector
会将所有对Selector
的操作全部代理给unwrappedSelector
,并在发起轮询IO事件
的相关操作中,重置SelectedSelectionKeySet
清空上一次的轮询结果。
到这里Reactor的核心Selector就创建好了,下面我们来看下用于保存异步任务的队列是如何创建出来的。
newTaskQueue
我们继续回到创建Reactor
的主线上,到目前为止Reactor
的核心Selector
就创建好了,前边我们提到Reactor
除了需要监听IO就绪事件
以及处理IO就绪事件
外,还需要执行一些异步任务,当外部线程向Reactor
提交异步任务后,Reactor
就需要一个队列来保存这些异步任务,等待Reactor线程
执行。
下面我们来看下Reactor
中任务队列的创建过程:
在
NioEventLoop
的父类SingleThreadEventLoop
中提供了一个静态变量DEFAULT_MAX_PENDING_TASKS
用来指定Reactor
任务队列的大小。可以通过系统变量-D io.netty.eventLoop.maxPendingTasks
进行设置,默认为Integer.MAX_VALUE
,表示任务队列默认为无界队列
。根据
DEFAULT_MAX_PENDING_TASKS
变量的设定,来决定创建无界任务队列还是有界任务队列。
Reactor
内的异步任务队列的类型为MpscQueue
,它是由JCTools
提供的一个高性能无锁队列,从命名前缀Mpsc
可以看出,它适用于多生产者单消费者
的场景,它支持多个生产者线程安全的访问队列,同一时刻只允许一个消费者线程读取队列中的元素。
我们知道Netty中的
Reactor
可以线程安全
的处理注册其上的多个SocketChannel
上的IO数据
,保证Reactor线程安全
的核心原因正是因为这个MpscQueue
,它可以支持多个业务线程在处理完业务逻辑后,线程安全的向MpscQueue
添加异步写任务
,然后由单个Reactor线程
来执行这些写任务
。既然是单线程执行,那肯定是线程安全的了。
Reactor对应的NioEventLoop类型继承结构

NioEventLoop
的继承结构也是比较复杂,这里我们只关注在Reactor
创建过程中涉及的到两个父类SingleThreadEventLoop
,SingleThreadEventExecutor
。
剩下的继承体系,我们在后边随着Netty
源码的深入在慢慢介绍。
前边我们提到,其实Reactor
我们可以看作是一个单线程的线程池,只有一个线程用来执行IO就绪事件的监听
,IO事件的处理
,异步任务的执行
。用MpscQueue
来存储待执行的异步任务。
命名前缀为SingleThread
的父类都是对Reactor
这些行为的分层定义。也是本小节要介绍的对象
SingleThreadEventLoop
Reactor
负责执行的异步任务分为三类:
普通任务:
这是Netty最主要执行的异步任务,存放在普通任务队列taskQueue
中。在NioEventLoop
构造函数中创建。定时任务:
存放在优先级队列中。后续我们介绍。尾部任务:
存放于尾部任务队列tailTasks
中,尾部任务一般不常用,在普通任务执行完后 Reactor线程会执行尾部任务。**使用场景:**比如对Netty 的运行状态做一些统计数据,例如任务循环的耗时、占用物理内存的大小等等都可以向尾部队列添加一个收尾任务完成统计数据的实时更新。
SingleThreadEventLoop
负责对尾部任务队列tailTasks
进行管理。并且提供Channel
向Reactor
注册的行为。
SingleThreadEventExecutor
SingleThreadEventExecutor
主要负责对普通任务队列
的管理,以及异步任务的执行
,Reactor线程的启停
。
到现在为止,一个完整的Reactor架构
就被创建出来了。

3. 创建Channel到Reactor的绑定策略
到这一步,Reactor线程组NioEventLoopGroup
里边的所有Reactor
就已经全部创建完毕。
无论是Netty服务端NioServerSocketChannel
关注的OP_ACCEPT
事件也好,还是Netty客户端NioSocketChannel
关注的OP_READ
和OP_WRITE
事件也好,都需要先注册到Reactor
上,Reactor
才能监听Channel
上关注的IO事件
实现IO多路复用
。
NioEventLoopGroup
(Reactor线程组)里边有众多的Reactor
,那么以上提到的这些Channel
究竟应该注册到哪个Reactor
上呢?这就需要一个绑定的策略来平均分配。
还记得我们前边介绍MultithreadEventExecutorGroup类
的时候提到的构造器参数EventExecutorChooserFactory
吗?
这时候它就派上用场了,它主要用来创建Channel
到Reactor
的绑定策略。默认为DefaultEventExecutorChooserFactory.INSTANCE
。
下面我们来看下具体的绑定策略:
DefaultEventExecutorChooserFactory
我们看到在newChooser
方法绑定策略有两个分支,不同之处在于需要判断Reactor线程组中的Reactor
个数是否为2的次幂
。
Netty中的绑定策略就是采用round-robin
轮询的方式来挨个选择Reactor
进行绑定。
采用round-robin
的方式进行负载均衡,我们一般会用round % reactor.length
取余的方式来挨个平均的定位到对应的Reactor
上。
如果Reactor
的个数reactor.length
恰好是2的次幂
,那么就可以用位操作&
运算round & reactor.length -1
来代替%
运算round % reactor.length
,因为位运算的性能更高。具体为什么&
运算能够代替%
运算,笔者会在后面讲述时间轮的时候为大家详细证明,这里大家只需记住这个公式,我们还是聚焦本文的主线。
了解了优化原理,我们在看代码实现就很容易理解了。
利用%
运算的方式Math.abs(idx.getAndIncrement() % executors.length)
来进行绑定。
利用&
运算的方式idx.getAndIncrement() & executors.length - 1
来进行绑定。
又一次被Netty对性能的极致追求所折服~~~~
4. 向Reactor线程组中所有的Reactor注册terminated回调函数
当Reactor线程组NioEventLoopGroup
中所有的Reactor
已经创建完毕,Channel
到Reactor
的绑定策略也创建完毕后,我们就来到了创建NioEventGroup
的最后一步。
俗话说的好,有创建就有启动,有启动就有关闭,这里会创建Reactor关闭
的回调函数terminationListener
,在Reactor
关闭时回调。
terminationListener
回调的逻辑很简单:
通过
AtomicInteger terminatedChildren
变量记录已经关闭的Reactor
个数,用来判断NioEventLoopGroup
中的Reactor
是否已经全部关闭。如果所有
Reactor
均已关闭,设置NioEventLoopGroup
中的terminationFuture
为success
。表示Reactor线程组
关闭成功。
我们在回到文章开头Netty服务端代码模板
现在Netty的主从Reactor线程组
就已经创建完毕,此时Netty服务端的骨架已经搭建完毕,骨架如下:

总结
本文介绍了首先介绍了Netty对各种IO模型
的支持以及如何轻松切换各种IO模型
。
还花了大量的篇幅介绍Netty服务端的核心引擎主从Reactor线程组
的创建过程。在这个过程中,我们还提到了Netty对各种细节进行的优化,展现了Netty对性能极致的追求。
好了,Netty服务端的骨架已经搭好,剩下的事情就该绑定端口地址然后接收连接了.
原文作者:bin的技术小屋
