vlambda博客
学习文章列表

深入源码去理解Netty线程池的设计思想


本篇文章大概2400字,阅读时间大约10分钟


本篇文章是对前面Netty线程图像相关文章的一个重新总结,并修改了一些错误,还是希望用面试题的形式来全面总结Netty的设计思想和使用的细节,本文主要是重点review了Netty的组件NioEventLoopGroup的初始化过程以及设计思想。不妨带着问题——为什么Netty要“吃饱了撑得”设计自己的线程池,JDK的线程池难道不香了么?】来阅读

深入源码去理解Netty线程池的设计思想

前面反复说到很多次,前提是讨论非阻塞模式下的线程池配置,此时Netty的线程池本质就是NioEventLoopGroup类,严谨的说就是

EventLoopGroup,参考:



以上只是表面的理解,还需要深入源码将该类的实例化过程捋清楚,才能真正明白Netty线程池到底是什么样子,通过这些分析,正好也能学习这种设计思路,有机会可以用到自己的项目中。


看服务端demo,我创建了两个线程池,只分析一个即可:

NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

在new NioEventLoopGroup时,经多个重载构造器的调用,最终进入其直接父类MultithreadEventLoopGroup的含参构造器。如下复习Netty线程池的类继承关系:

深入源码去理解Netty线程池的设计思想

看MultithreadEventLoopGroup类的含参构造器,如下它会判断传入的线程参数nThreads的大小来决定创建多少线程。

深入源码去理解Netty线程池的设计思想

其中的常量DEFAULT_EVENT_LOOP_THREADS是Netty默认提供的线程数,大小为2倍CPU核数个,即如果不指定线程数(nThreads等于0时,这个0是最初的默认构造器设置),那么Netty会默认为其创建2*cpu核数个线程。如下源码是初始化该常量的逻辑:

深入源码去理解Netty线程池的设计思想

在我的服务端demo中,bossGroup设置了1个线程,workerGroup的线程数是默认值——CPU核数乘2。这样设置的NioEventLoopGroup就是Reactor多线程模型。


继续看MultithreadEventLoopGroup构造器,它在设置好线程数后会继续调用父类MultithreadEventExecutorGroup的构造器,如下:

深入源码去理解Netty线程池的设计思想

MultithreadEventExecutorGroup构造器主要做了三件事:

1、创建NioEventLoopGroup的线程执行器——ThreadPerTaskExcutor

它是对Java的线程池接口的实现,负责创建NioEventLoopGroup的底层Java线程Thread,如下ThreadPerTaskExcutor源码片段:

深入源码去理解Netty线程池的设计思想

当然,如果用户传入了自己实现的Executor,那么这里就不会使用默认配置。


2、for循环+newChild(executor,args)方法,去构造nThreads个NioEventLoop对象。如下源码片段,还是在这个构造器:

深入源码去理解Netty线程池的设计思想

注意到第一行红线处,即for循环前面的属性children,它的类型是一个数组,即EventExecutor[],由此可知EventLoopGroup实现的线程池其实就是类MultithreadEventExecutorGroup内部维护的类型为EventExecutor[]的数组,其大小是nThreads,而EventExecutor本身是一个Netty线程相关的接口,这样就构成了一个Netty线程池。其中,for循环调用newChild抽象方法,这里也是模板方法模式的运用,参考:


看第二个红线处,在newChild构建线程时,同时会将刚刚创建的线程执行器——ThreadPerTaskExcutor当做参数传入,去合力创建线程池线程,这个线程就是前面说的NioEventLoop对象,参考:


如下,看看这个newChild抽象方法的实现,发现它最终会调用到NioEventLoopGroup类覆写的newChild方法,里面返回了一个NioEventLoop实例:

深入源码去理解Netty线程池的设计思想

显然线程执行器——ThreadPerTaskExcutor会辅助创建NioEventLoopGroup对应的底层线程——NioEventLoop


3、第2步的for循环完成后,接着创建NioEventLoopGroup的线程选择器——chooser:

深入源码去理解Netty线程池的设计思想

chooser的目的是可以给每个新连接Channel分配NioEventLoop线程。具体细节后续专题总结。


以上,一个线程池就构建完毕,前面提到在newChild方法中,会实例化NioEventLoop对象,这里的细节下一篇分析,但是这里要知道实例化过程会顺便为该对象绑定一个队列,即所谓的异步任务队列,也叫多生产者单消费者队列,英文简写为MPSCQ,这样,Netty线程池完整的架构图如下:


深入源码去理解Netty线程池的设计思想

以上,就是初始化一个Netty线程池需要做的事情,它有创建线程的组件,有分配线程的组件,即图里的线程轮询器,也就是刚刚提到的chooser,还有额外驱动线程的组件——线程执行器,这样就实现了职责分离,这就是好的设计思想。同时Netty给每个NIO线程都绑定了一个异步任务队列,每个线程始终执行自己队列里的任务,而不是和JDK线程池那样多个线程竞争一个阻塞队列。这样的好处,一方面是不需要加锁,另一方面是符合NIO的业务需求,更重要的是还能规避一些NIO的API的坑,所以Netty线程池是一种局部串行无锁化的线程池设计,它和JDK线程池长得完全不一样。


总结:

1、实例化NioEventLoopGroup时,如果指定线程池大小,那么nThreads就是指定的值,反之是机器的处理器核心数*2个


2、boss线程池没必要设置多个线程数,因为针对一个服务端端口,实际起作用的I/O线程只有一个。关于线程池线程数目的配置,需要理解I/O密集型任务和CPU密集型任务的区别


3、每个NioEventLoopGroup都对应了一个线程执行器,默认是Netty提供的ThreadPerTaskExcutor,作用是创建NioEventLoopGroup底层JDK的线程对象——Thread,用户也可以自己实现并当做参数传入


4、EventLoopGroup(其实是MultithreadEventExecutorGroup类)内部,维护了一个类型为EventExecutor[]的children数组,大小是nThreads,这样就构成了一个线程池,也就是所谓的Netty线程池的本来面目


5、MultithreadEventExecutorGroup构造器会调用newChild抽象方法(模板方法模式的实现),并结合线程执行器来赋值这个children数组,该方法会返回NioEventLoop实例,同时给每个NIO线程都附加一个任务队列,实现局部串行无锁化线程池。


6、提供了分配线程的组件,主要是为了给新来的Channel分配线程,也叫线程选择器,我更细化叫线程轮询器,它也是NioEventLoopGroup构造器内创建


既然说到了Netty线程池的设计,这里补充其线程执行器的实现,重温NioEventLoopGroup的继承关系:

深入源码去理解Netty线程池的设计思想

前面分析了不指定Executor,直接实例化NioEventLoopGroup,Netty会为其创建默认的线程执行器——ThreadPerTaskExcutor(),它顾名思义是负责创建NioEventLoopGroup对应的Java线程。这很好理解,所谓的线程执行器就是JDK的线程池接口,当然是负责创建线程了。


如下,刚刚分析过MultithreadEventExecutorGroup类中Netty线程池创建的核心片段:

深入源码去理解Netty线程池的设计思想

下面,看这个线程执行器的构造器:

深入源码去理解Netty线程池的设计思想

见名知意,线程执行器——ThreadPerTaskExecutor,其中的Per一般用来代表每个的意思,即每创建一个连接,Netty都会为其绑定一个Thread。


线程执行器主要作用有两个:

1、为Netty创建的NioEventLoop线程,配置自定义的命名规则,该功能的实现本质就是实现了JDK的ThreadFactory接口,JDK注释也写得很清楚,提供一个解耦,避免硬编码创建Thread的方式,用工厂这种模式替代:

深入源码去理解Netty线程池的设计思想


2、在每次执行异步任务队列里的task时,Netty的线程执行器都会创建一个Netty自己封装的线程实体FastThreadLocalThread,这里的task可以理解为就是JDK的Runnable对象,泛化的说是Netty的业务逻辑单元,一般都是异步执行,后续细说。


下面重点分析线程执行器的核心逻辑,进入到ThreadPerTaskExecutor类,发现它实现的是JUC包里的Executor接口,故覆写了execute方法,目的是想自己来控制线程的创建和启动的细节。

深入源码去理解Netty线程池的设计思想

ThreadPerTaskExcutor会持有一个线程工厂——ThreadFactory的实例,注意这里要对JDK的JUC包很熟悉,因为ThreadFactory也是JUC的一个接口,用来屏蔽传统的线程创建的硬编码形式,JDK也建议使用这个线程工厂来创建线程,因此Netty遵循了这个建议,并且能根据这个接口,为Netty创建的NioEventLoop线程配置自定义的命名规则。线程执行器每次执行任务都会调用execute方法创建线程,并启动线程,本质还是用ThreadFactory的相关API实现的。


下面简单看这个线程工厂——ThreadFactory实体是谁来负责实例化的。回忆前面在服务端demo创建NioEventLoopGroup时,最终会调用到MultithreadEventExecutorGroup:

深入源码去理解Netty线程池的设计思想

创建线程执行器的时候,从其构造器的参数传入了这个线程工厂——通过newDefaultThreadFactory()方法创建,源码如下:

深入源码去理解Netty线程池的设计思想

调用到子类MultithreadEventLoopGroup中,通过Object的getClass()方法获取当前类的class对象,这个class对象就是poolType,它会被传入DefaultThreadFactory类的构造器。如下:

深入源码去理解Netty线程池的设计思想

在看一下这个参数:

深入源码去理解Netty线程池的设计思想


拿到的poolType参数,以及默认线程优先级等,就可以在该构造器里设置Netty线程的一些默认属性,接下来,继续调用DefaultThreadFactory重载构造器,进入下面的构造器:

深入源码去理解Netty线程池的设计思想

当然还有其他构造器,这样层层调用,最终为该线程设置一系列属性,包括是否是后台线程,线程优先级指定等,其中定义有意义的线程池名和线程名是最重要的,可以辅助快速排查异常,线程执行器就这是这么多内容。


小结:Netty的线程执行器是一个非常好的自定义线程池实现,它自己实现了ThreadFactory接口,扩展了自己的线程池的命名规则等,可以模仿应用到项目中。


完整的Netty线程池的样子如下:

深入源码去理解Netty线程池的设计思想



END


点亮在看,你最好看

~

阅读原文,获得更多精彩内容