一篇学会NioEventLoopGroup源码解析

网络 通信技术
这里我们会创建一个线程执行器 ThreadPerTaskExecutor,使用默认的线程工厂DefaultThreadFactory,线程执行器会将一个任务包装为一个 FastThreadLocalThread对象,然后调用start方法开启一个新的线程执行任务!

[[408806]]

本文转载自微信公众号「源码学徒」,作者皇甫嗷嗷叫 。转载本文请联系源码学徒公众号。

NioEventLoopGroup的初始化源码

一、寻找源码的过程

我们前面说到过,NioEventLoopGroup我们可以近乎把它看作是一个线程池,该线程池会执行一个一个的任务,我们常用的NioEventLoopGroup大概有两种,NioEventLoopGroup(int nThreads),NioEventLoopGroup(),即一个是指定线程数量的,一个是默认指定线程数量的!这里我们以无参构造为入口进行分析!

EventLoopGroup work = new NioEventLoopGroup(); 
  • 1.
public NioEventLoopGroup() { 
    this(0); 

  • 1.
  • 2.
  • 3.

当我们使用默认的数量的时候,他会传递一个0,我们继续往下跟!

public NioEventLoopGroup(int nThreads) { 
    this(nThreads, (Executor) null); 

  • 1.
  • 2.
  • 3.

注意这里传递的参数是:0,null

public NioEventLoopGroup(int nThreads, Executor executor) { 
    //每个 group维护一个 SelectorProvider 主要用它获取selector选择器 
    this(nThreads, executor, SelectorProvider.provider()); 

  • 1.
  • 2.
  • 3.
  • 4.

这里面多传递了一个 SelectorProvider.provider(),该方法是JDK NIO提供的API主要可以获取NIO选择器或者Channel,如下图:

我们回归主线继续跟:

public NioEventLoopGroup( 
            int nThreads, Executor executor, final SelectorProvider selectorProvider) { 
    this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); 

  • 1.
  • 2.
  • 3.
  • 4.

这里多传递了一个 DefaultSelectStrategy选择策略,这在后面讲解NioEventLoop会具体讲解,不做阐述!

public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider, 
                             final SelectStrategyFactory selectStrategyFactory) { 
    super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); 

  • 1.
  • 2.
  • 3.
  • 4.

我们会发现这里还会默认传递一个拒绝策略RejectedExecutionHandlers.reject(),这个拒绝策略是干嘛的呢?

@Override 
public void rejected(Runnable task, SingleThreadEventExecutor executor) { 
    throw new RejectedExecutionException(); 

  • 1.
  • 2.
  • 3.
  • 4.

我们得到一个结论,当某些条件触发这个拒绝策略,那么他会抛出一个RejectedExecutionException异常,具体什么时候触发,后续也会详细说明,这里只需要记住就OK了!

我们继续回到主线, 这里我们开始调用父类,还记得上一节课我们分析的NioEventLoopGroup的父类是谁吗?没错是:MultithreadEventLoopGroup, 我们会进入到MultithreadEventLoopGroup里面:

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { 
    //线程数量为0时  使用默认的cpu * 2 
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); 

  • 1.
  • 2.
  • 3.
  • 4.

nThreads还记得是几吗?是0对不对,这里有个判断,当你的线程数量为0的时候,会使用DEFAULT_EVENT_LOOP_THREADS当作线程池的数量,DEFAULT_EVENT_LOOP_THREADS是多少呢?

DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); 
  • 1.

默认是CPU的两倍,所以我们现在得到一个结论,当我们使用默认的NioEventLoopGroup的时候,系统会默认使用系统CPU核数*2当作线程池的数量!

我们上一步传递过来的selectorProvider、拒绝策略、selectStrategyFactory被封装为数组,并放在args[0],args[1], args[2]的位置!

我们继续回到主线,这里又再次调用到父类,MultithreadEventExecutorGroup:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) { 
    this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args); 

  • 1.
  • 2.
  • 3.

注意,这里又再次多传递了一个参数:DefaultEventExecutorChooserFactory一个选择器工厂,这里会返回一个选择器,他是DefaultEventExecutorChooserFactory类型的,具体分析后面会分析!我们继续回到主线:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { 
    ..........后续源码补充.......... 

  • 1.
  • 2.
  • 3.

到这里我们终于看到了一大段代码,这里是EventLoopGroup的主要逻辑,我们逐行分析:

二、构建线程执行器

1. 源码解析

//newDefaultThreadFactory  构建线程工厂 
if (executor == null) { 
    //创建并保存线程执行器  执行器  执行任务的  默认是  DefaultThreadFactory 线程池 
    executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这里会判断我们传入的执行器是否为空,否则就新建一个,我们还记得executor是什么值吗?是null,对不对,所以它会进入到这里的逻辑我们进入到newDefaultThreadFactory源码里面看一下:

newDefaultThreadFactory()主要逻辑

protected ThreadFactory newDefaultThreadFactory() { 
    return new DefaultThreadFactory(getClass()); 

  • 1.
  • 2.
  • 3.

可以看到,这里向执行器里面传入了一个 DefaultThreadFactory一个默认的线程工厂!

ThreadPerTaskExecutor主要逻辑:

/** 
 * io.netty.util.concurrent.DefaultThreadFactory#newThread(java.lang.Runnable) 
 * 
 * 执行任务  每次执行任务都会创建一个线程实体对象 
 * @param command 线程 
 */ 
@Override 
public void execute(Runnable command) { 
    //执行任务 
    threadFactory.newThread(command).start(); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

我们发现,这里调用了一个我们传入的线程工厂,创建了一个新的线程并调用start方法启动了起来,那么他是如何创建的呢? 我们进入到newThread源码里面查看,由于我们默认使用的线程工厂是 DefaultThreadFactory, 所以,我们会进入到 DefaultThreadFactory#newThread

@Override 
public Thread newThread(Runnable r) { 
    //创建一个线程 每次执行任务的时候都会创建一个线程实体 
    Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet()); 
    try { 
        if (t.isDaemon() != daemon) { 
            t.setDaemon(daemon); 
        } 
 
        if (t.getPriority() != priority) { 
            t.setPriority(priority); 
        } 
    } catch (Exception ignored) { 
        // Doesn't matter even if failed to set
    } 
    return t; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

这里没有太多的操作,只是会将一个 Runnable封装为一个 Thread进行返回,我们重点关注一下这个Thread,它和我们传统使用的Thread是一样的吗? 我们跟进到 newThread方法看一下:

protected Thread newThread(Runnable r, String name) { 
    //Netty自己封装的线程 
    return new FastThreadLocalThread(threadGroup, r, name); 

  • 1.
  • 2.
  • 3.
  • 4.

逻辑很简单,就是将一个Thread包装为Netty自定义的 FastThreadLocalThread,至于为什么,我们暂时不往下多做解释,后续章节会很详细的解释它!

2. 线程执行器总结

这里我们会创建一个线程执行器 ThreadPerTaskExecutor,使用默认的线程工厂DefaultThreadFactory,线程执行器会将一个任务包装为一个 FastThreadLocalThread对象,然后调用start方法开启一个新的线程执行任务!

三、创建对应数量的执行器

//创建执行器数组  数量和预设线程数量一致 
children = new EventExecutor[nThreads]; 
 
for (int i = 0; i < nThreads; i ++) { 
    boolean success = false
    try { 
        //创建执行器  开始创建执行器   这里的执行机估计就会EventLoop  是NioEventLoop 
        children[i] = newChild(executor, args); 
        success = true
    } catch (Exception e) { 
        .....省略不必要代码 
    } finally { 
        .....省略不必要代码 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

1. 源码解析

children = new EventExecutor[nThreads]; 
  • 1.

首先他会创建一个空的EventExecutor执行器数组,然后遍历填充!

还记得 nThreads是几吗? 默认是CPU*2的大小,所以这里会创建 CPU * 2数量的执行器! 我们发现,for循环中填充的主要逻辑是newChild,所以,我们进入到 newChild方法, 这里提示一点,我们创建的Group对象是一个什么对象? 是NioEventLoopGroup对象对不对,所以我们这里会进入到 NioEventLoopGroup#newChild方法:

@Override 
protected EventLoop newChild(Executor executor, Object... args) throws Exception { 
    EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null
    return new NioEventLoop(this, executor, (SelectorProvider) args[0], 
                            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

我们传递过来 的args长度为3,前面做过解析 :

args[0]为 selectorProvider、args[1]为拒绝策略、args[2]为selectStrategyFactory

所以 queueFactory为null, 然后我们再重点关注 NioEventLoop对象,可以看出,newChild方法返回的是 NioEventLoop,那么我们初步就可以确定,EventExecutor数组里面存在的是NioEventLoop对象!至此,我们就不深究了,NioEventLoop的初始化源码分析我会放到下一节课分析,这里我们可以确定一件事, EventExecutor数组里面存在的是NioEventLoop对象!我们继续回到主线:

2. 执行器数组总结

for循环完毕之后,此时的EventExecutor[nThreads];数组就被填充满了,里面的每一个元素都是NioEventLoop对象,每一个NioEventLoop对象都包含一个 ThreadPerTaskExecutor线程执行器对象!

四、创建一个执行器选择器

1. 源码解析

chooser = chooserFactory.newChooser(children); 
  • 1.

还记得 chooserFactory是什么类型的吗? 是DefaultEventExecutorChooserFactory类型的,忘了的可以往上翻一下寻找源码的过程中的代码或者调试一下!

我们进入到 DefaultEventExecutorChooserFactory#newChooser 源码逻辑中,并传入刚刚我们循环填充好的数组:

@Override 
public EventExecutorChooser newChooser(EventExecutor[] executors) { 
    //判断2的幂 isPowerOfTwo 
    if (isPowerOfTwo(executors.length)) { 
        return new PowerOfTwoEventExecutorChooser(executors); 
    } else { 
        //简单的 
        return new GenericEventExecutorChooser(executors); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

可以看到,这里似乎有两种情况,返回的是不同的策略对象,当你的数组长度是2的幂等次方的时候,返回的是 PowerOfTwoEventExecutorChooser对象,否则返回 GenericEventExecutorChooser对象,我们就两种情况全部分析一下:

I、PowerOfTwoEventExecutorChooser

PowerOfTwoEventExecutorChooser(EventExecutor[] executors) { 
    this.executors = executors; 

 
@Override 
public EventExecutor next() { 
    //2的幂等性  实现这个  也能实现循环取数的 
    //executors   就是NioEventLoop数组  按照2次幂求本次获取的EventLoop是个啥 
    return executors[idx.getAndIncrement() & executors.length - 1]; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这段代码的主要逻辑是,取一个自增的CAS类,与数组长度做&运算,最终会出现循环取数的结果:

从上面的图片可以基本看出来, 该功能可以实现一个循环取数的功能,每次达到数组的尾部部都会重新回到头部重新获取!

代码案例:

public static void main(String[] args) { 
    String[] strings = {"第一个""第二个""第三个""第四个"}; 
    AtomicInteger idx = new AtomicInteger(); 
    for (int i = 0; i < 9; i++) { 
        System.out.println(strings[idx.getAndIncrement() & strings.length -1]); 
    } 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

结果集

第一个 
第二个 
第三个 
第四个 
第一个 
第二个 
第三个 
第四个 
第一个 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

II、GenericEventExecutorChooser

当你的线程数量不是2的幂次方的时候,会走一个通用的选择器,具体实现源码如下:

GenericEventExecutorChooser(EventExecutor[] executors) { 
    this.executors = executors; 

 
@Override 
public EventExecutor next() { 
    //自增  取模   以达到循环的目的 
    //假设executors 长度为5  那么 不断的循环就会不断的得到 0 1 2 3 4  0 1 2 3 4。。。 
    return executors[Math.abs(idx.getAndIncrement() % executors.length)]; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这个代码就不用了我做演示了吧,他的功能和上面那个功能是一样的能 能够达到一个循环取数的功能

思考

为什么Netty要分为两个策略类来实现呢,直接用第二种不行吗?

Netty官方对性能的要求达到了极致,大家要知道位运算速度要高于直接取模运算的,所以Netty官方即使是这一点也做了一个优化!

2. 执行器选择器总结

我们通过上述可以了解到,这里会通过一个选择器工厂创建一个选择器,并保存在NioEvenetLoopGroup中,调用该选择器的next方法会返回一个NioEventLoop对象,其中的获取方式是不断的循环,依次获取NioEventLoop对象,这也是一个NioEventLoop对SocketChannel为一对多的基础!这都是后话!

NioEventLoopGroup源码总结

 

  1. 创建一个线程执行器,当调用该线程执行器的execute方法的时候,会讲一个Runable对象包装为Thread对象,再将Thread对象包装为FastThreadLocalThread对象,然后启动起来! 简单来说,每调用一次execute方法,都去创建并启动一条新线程执行任务!
  2. 创建一个执行器数组,数组长度与我们传递的数量有关,默认为CPU*2个数量,然后再循环填充这个空数组,数组里面的元素是一个NioEventLoop对象,每一个NioEventLoop对会持有一个线程执行器的引用!
  3. 创建一个执行器选择器,调用该执行器选择器的next方法可以返回一个NioEventLoop对象,内部是进行循环取数的,每一个NioEventLoop都可能会被多次获取!

 

责任编辑:武晓燕 来源: 源码学徒
相关推荐

2022-06-09 08:41:17

Go网络库Gnet

2021-07-12 22:50:29

Caffeine数据结构

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2021-07-06 08:59:18

抽象工厂模式

2021-05-11 08:54:59

建造者模式设计

2021-07-02 09:45:29

MySQL InnoDB数据

2023-01-03 08:31:54

Spring读取器配置

2021-07-05 22:11:38

MySQL体系架构

2022-08-26 09:29:01

Kubernetes策略Master

2023-11-28 08:29:31

Rust内存布局

2022-08-23 08:00:59

磁盘性能网络

2022-04-12 08:30:52

回调函数代码调试

2023-11-01 09:07:01

Spring装配源码

2021-10-27 09:59:35

存储

2021-07-16 22:43:10

Go并发Golang

2021-10-14 10:22:19

逃逸JVM性能

2021-04-29 10:18:18

循环依赖数组

2023-03-13 21:38:08

TCP数据IP地址

2022-11-14 08:17:56

点赞
收藏

51CTO技术栈公众号