创建线程的三种方法_Netty源码分析系列之NioEventLoop的创建与启动

dccd6b9ddab8c7a99e45f4a4002a49b0.png

前言

前三篇文章分别分析了 Netty 服务端 channel 的初始化、注册以及绑定过程的源码,理论上这篇文章应该开始分析新连接接入过程的源码了,但是在看源码的过程中,发现有一个非常重要的组件:NioEventLoop,出现得非常频繁,以至于影响到了后面源码的阅读,因此决定先分析下NioEventLoop的源码,再分析新连接接入的源码。关于NioEventLoop这个组件的源码分析,将会写两篇文章来分享。第一篇文章将主要分析NioEventLoop 的创建与启动,第二篇将主要分析NioEventLoop 的执行流程

在开始之前,先来思考一下一下两个问题。

  1. Netty 中的线程是何时启动的?
  2. Netty 中的线程是如何实现串行无锁化的?

功能说明

NioEventLoop 从功能上,可以把它当做一个线程来理解,当它启动以后,它就会不停地循环处理三种任务(从类名上也能体现出循环处理的思想:Loop)。这三种任务分别是哪三种任务呢?

  1. 网络 IO 事件;
  2. 普通任务。通过调用execute(Runnable task) 来执行普通任务。
  3. 定时任务。通过调用schedule(Runnable task,long delay,TimeUnit unit) 来执行定时任务。

NioEventLoop 类的继承关系特别复杂,它的 UML 图如下。

4615d37bf1695e60994fc59b729f637c.png

从图中可以看到,它实现了ScheduledExecutorService接口,因此它可以实现定时任务相关的功能;同时它还继承了SingleThreadEventExecutor类,从类名看,这是一个单线程的线程执行器。

创建流程

在 netty 中,我们通过NioEventLoopGroup来创建NioEventLoop,入口就是下面这一行代码。

EventLoopGroup workerGroup = new NioEventLoopGroup()

当使用NioEventLoopGroup的无参构造器时,netty 会默认创建2 倍 CPU 核数数量的 NioEventLoop;当使用 NioEventLoopGroup 的有参构造方法时,向构造方法中传入一个 int 值,就表示创建指定个数的 NioEventLoop。无论是使用 NioEventLoopGroup 有参构造方法,还是无参构造方法,最终都会调用到 NioEventLoopGroup 类中的如下构造方法。

public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {// 调用父类super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}

这个构造方法有很多参数,此时每个参数的解释如下。

  • nThreads:要创建的线程的数量,如果前面使用的是 NioEventLoopGroup 无参构造器,此时 nThreads 的值为 0,如果使用的是 NioEventLoopGroup 的有参构造方法,nThreads 的值为构造方法中传入的值。
  • executor:线程执行器,默认是 null,这个属性的值会在后面创建 NioEventLoop 时,进行初始化。用户可以自定义实现 executor,如果用户自定义了,那么此时 executor 就不为 null,后面就不会再进行初始化。
  • selectorProvider:SelectorProvider 类型,它是通过SelectorProvider.provider() 创建出来的,这是 JDK 中 NIO 相关的 API,会创建出一个 SelectorProvider 对象,这个对象的作用就是创建多路复用器 Selector 和服务端 channel。
  • selectStrategyFactory:选择策略工厂,通过 DefaultSelectStrategyFactory.INSTANCE 创建,INSTANCE这个常量的值又是通过new DefaultSelectStrategyFactory() 来创建的。
  • RejectedExecutionHandlers.reject():返回的是一个拒绝策略,当向线程池中添加任务时,如果线程池任务队列已满,这个时候任务就会被拒绝,此时线程池就会执行拒绝策略。

接着又会调用父类的构造方法,NioEventLoopGroup直接继承了MultithreadEventLoopGroup类,此时会调用到MultithreadEventLoopGroup的如下构造方法。

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

可以看到,构造方法的参数中,有一个 args 参数,是一个 Object 类型的可变数组。因此当在 NioEventLoopGroup 的构造方法调用到父类中时,selectorProvider、selectStrategyFactory、RejectedExecutionHandlers.reject() 都变成了 args 这个可变数组中的元素了。另外,我们从代码中可以知道,如果前面传递过来的 nThread 为 0,那么就令 nThread 的值等于DEFAULT_EVENT_LOOP_THREADS,而DEFAULT_EVENT_LOOP_THREADS这个常量的值就是 2 倍的 CPU 核数;如果前面传递过来的 nThread 不为 0,就使用传递过来的 nThread。

接着继续向上调用父类的构造器,MultithreadEventLoopGroup 继承了MultithreadEventExecutorGroup类,因此会调用到MultithreadEventExecutorGroup的如下构造方法。

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

其中nThreads、executor、args这些参数就是前面传过来,这里就不多说了。然后通过DefaultEventExecutorChooserFactory.INSTANCE创建的是一个事件执行选择工厂,INSTANCE 常量的值是通过new DefaultEventExecutorChooserFactory() 创建出来的对象。 接着又通过 this 调用了 MultithreadEventExecutorGroup 类中的另一个构造方法,接下来这个构造方法就是核心代码了。该构造方法的代码很长,为了方便阅读,我进行了精简,精简后的源码如下。

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,EventExecutorChooserFactory chooserFactory, Object... args) {if (nThreads <= 0) {throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));}if (executor == null) {/*** 创建线程执行器:ThreadPerTaskExecutor* newDefaultThreadFactory()会创建一个线程工厂,该线程工厂的作用就是用来创建线程,同时给线程设置名称:nioEventLoop-1-XX*/executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}// 根据传进来的线程数,来创建指定大小的数组大小,这个数组就是用来存放NioEventLoop对象实例children = new EventExecutor[nThreads];for (int i = 0; i < nThreads; i ++) {//出现异常标识boolean success = false;try {//创建nThreads个nioEventLoop保存到children数组中children[i] = newChild(executor, args);success = true;} catch (Exception e) {throw new IllegalStateException("failed to create a child event loop", e);} finally {// 异常处理...}}// 通过线程执行器选择工厂来创建一个线程执行器chooser = chooserFactory.newChooser(children);// 省略部分代码...
}

这个方法中,有三处主要的逻辑。第一处逻辑:当 executor 为空时,创建一个ThreadPerTaskExecutor类型的线程执行器;第二处逻辑:通过newChild(executor, args) 来创建NIoEventLoop;第三处:通过chooserFactory.newChooser(children) 来创建一个线程执行器的选择器。下面将逐步详细分析这三处逻辑。

创建线程执行器

if (executor == null) {/*** 创建线程执行器:ThreadPerTaskExecutor* newDefaultThreadFactory()会创建一个线程工厂,该线程工厂的作用就是用来创建线程,同时给线程设置名称:nioEventLoop-1-XX*/executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}

在第一处核心逻辑处,首先判断 executor 是否为空,如果用户没有自己指定,默认情况下,executor 是 null,因此就会通过 new 关键字来创建一个ThreadPerTaskExecutor类型的线程执行器。

在调用ThreadPerTaskExecutor的构造方法之前,先通过new DefaultThreadFactory() 创建了一个线程工厂,该线程工厂是DefaultThreadFactory类型,它实现了 ThreadFactory 接口,它的作用就是:当调用 threadFactory 的 newThread()方法时,就会创建出一个线程,同时给线程取一个有意义的名称,名称生成规则为:nioEventLoop-xx-xx。第一个 xx 的含义表示的 NiEventLoopGroup 的组号,在 netty 中可能同时创建 bossGroup 和 workerGroup 两个线程组,所以第一个 xx 表示线程组的序号。第二个 xx 表示的是线程在线程组中的序号。如:nioEventLoop-1-1 表示的是该线程是第一个 NioEventLoopGroup 线程组的第一个线程。

ThreadPerTaskExecutor类的源码比较简单,它实现了Executor接口,重写了execute() 方法,当每次调用ThreadPerTaskExecutor类的execute() 方法时,会创建一个线程,并启动线程。这里可能会有一个疑问:每次调用execute() 方法,都会创建一个线程,岂不是意味着会创建很多线程?实际上,在每个NioEventLoop中,只会调用一次ThreadPerTaskExecutorexecute() 方法,因此对于每个NioEventLoop而言,只会创建一个线程,且当线程启动后,就不会再调用ThreadPerTaskExecutorexecute() 方法了,也就不会造成在系统中创建多个线程。

public final class ThreadPerTaskExecutor implements Executor {private final ThreadFactory threadFactory;public ThreadPerTaskExecutor(ThreadFactory threadFactory) {if (threadFactory == null) {throw new NullPointerException("threadFactory");}this.threadFactory = threadFactory;}@Overridepublic void execute(Runnable command) {// threadFactory就是前面创建的DefaultThreadFactory// 通过线程工厂的newThread()方法来创建一个线程,并启动线程threadFactory.newThread(command).start();}
}

创建 NioEventLoop

// 根据传进来的线程数,来创建指定大小的数组大小,这个数组就是用来存放NioEventLoop对象实例
children = new EventExecutor[nThreads];for (int i = 0; i < nThreads; i ++) {//出现异常标识boolean success = false;try {//创建nThreads个nioEventLoop保存到children数组中children[i] = newChild(executor, args);success = true;} catch (Exception e) {throw new IllegalStateException("failed to create a child event loop", e);} finally {// 异常处理...}
}

在执行第二处核心逻辑之前,先创建了一个EventExecutor类型的数组,数组的大小就是前面传进来的线程个数,然后将数组赋值给children属性,这个属性是 NioEventLoopGroup 的属性,NioEventLoopGroup 包含一组 NioEventLoop 线程,children 属性就是用来存放这一组 NioEventLoop 线程的。此时只是创建出了数组,但是数组中的元素都是 null,所以接下来通过 for 循环来为数组填充元素,通过newChild(executor, args) 创建出一个 NioEventLoop 对象,然后将对象赋值给数组中的元素。

当调用newChild(executor, args) 方法时,第一个参数 executor 就是上一步创建出来的ThreadPerTaskExecutor对象,第二个参数是一个可变数组,它的每一个元素是什么,有什么作用,在前面已经解释过了。newChild(executor, args) 定义在 NioEventLoopGroup 类中,源码如下。

protected EventLoop newChild(Executor executor, Object... args) throws Exception {/*** executor: ThreadPerTaskExecutor* args:  args是一个可变数组的参数,实际上它包含三个元素,也就是前面传递过来的三个参数,如下:*          SelectorProvider.provider()是JDK中NIO相关的API,会创建出一个SelectorProvider,它的作用就是在后面创建多路复用器Selector和服务端channel*          DefaultSelectStrategyFactory.INSTANCE 是一个默认选择策略工厂,new DefaultSelectStrategyFactory()*          RejectedExecutionHandlers.reject()返回的是一个拒绝策略,当向线程池中添加任务时,如果线程池任务队列已满,这个时候任务就会被拒绝,然后执行拒绝策略**/return new NioEventLoop(this, executor, (SelectorProvider) args[0],((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}

可以看见,在newChild() 中直接调用了 NioEventLoop 的构造方法。NioEventLoop 的构造方法源码如下。

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {/*** executor: ThreadPerTaskExecutor* args:  args是一个可变数组的参数,实际上它包含三个元素,也就是前面传递过来的三个参数,如下:*          SelectorProvider.provider()是JDK中NIO相关的API,会创建出一个SelectorProvider,它的作用就是在后面创建多路复用器Selector和服务端channel*          DefaultSelectStrategyFactory.INSTANCE 是一个默认选择策略工厂,new DefaultSelectStrategyFactory()*          RejectedExecutionHandlers.reject()返回的是一个拒绝策略,当向线程池中添加任务时,如果线程池任务队列已满,这个时候任务就会被拒绝,然后执行拒绝策略**/super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);if (selectorProvider == null) {throw new NullPointerException("selectorProvider");}if (strategy == null) {throw new NullPointerException("selectStrategy");}provider = selectorProvider;// openSelector()方法会创建一个多路复用器,但是这个多路复用器的selectedKey的底层数据接口被替换了final SelectorTuple selectorTuple = openSelector();//替换了数据结构selectedKeys   publicSelectedKeys的原生selectorselector = selectorTuple.selector;//子类包装的selector  底层数据结构也是被替换了的unwrappedSelector = selectorTuple.unwrappedSelector;selectStrategy = strategy;
}

在 NioEventLoop 的构造方法中主要干了两件事,一是继续向上调用父类的构造方法,二是调用openSelector() 方法。在父类的构造方法中,会初始化两个任务队列:tailTasks 和 taskQueue,最终两个属性创建出来的都是MpscQueue类型的队列,同时还将传入的 executor 进行了一次包装,通过 ThreadExecutorMap 将其包装成了一个匿名类。(MpscQueue 是个什么东西呢?它是many producer single consumer的简写,意思就是同一时刻可以有多个生产者往队列中存东西,但是同一时刻只允许一个线程从队列中取东西。)

接着是调用openSelector() 来创建多路复用器 Selector,然后将多路复用器保存到 NioEventLoop 当中,在这一步 Netty 对多路复用器进行了优化。原生的 Selector 底层存放 SelectionKey 的数据结构是HashSetHashSet 在极端情况下,添加操作的时间复杂度是 O(n) ,Netty 则将 HashSet 类型替换成了数组类型,这样添加操作的时间复杂度始终是 O(1) 。openSelector()方法的源码很长,下面以图片的方式贴出其源码,你也可以直接跳过源码,看我后面的总结。

ca592240505112796273d2d85c0a8c78.png

openSelector()方法的源码很长,经过整理后,可以总结为如下几个步骤:

  • 先调用 JDK 的 API 创建多路复用器 Selector:provider.openSelector();
  • 通过DISABLE_KEY_SET_OPTIMIZATION属性判断是否禁用优化,如果为 true,则表示不进行底层数据结构的替换,即不优化,直接返回原生的 Selector。DISABLE_KEY_SET_OPTIMIZATION常量的含义是是否禁用优化:即是否禁止替换底层数据结构,默认为 false,不禁止优化。可以通过 io.netty.noKeySetOptimization 来配置。
  • 通过反射加载 SelectorImpl:Class.forName("sun.nio.ch.SelectorImpl",false,PlatformDependent.getSystemClassLoader())
  • 通过反射获取原生 SelectorImpl 中的selectedKeys、publicSelectedKeys属性(这两个属性的数据类型是HashSet 类型),然后再将这两个属性的访问权限设置为 true,接着再通过反射,将selectedKeys、publicSelectedKeys这连个属性的类型替换为 Netty 中自定义的数据类型:SelectedSelectionKeySet。该类型的底层数据结构是数组类型
  • 最后将SelectedSelectionKeySet封装到 netty 自定义的多路复用器SelectedSelectionKeySetSelector中,然后将 JDK 原生的 Selector 和 Netty 自定义的 Selector 封装到SelectorTuple中,再将SelectorTuple返回。注意:这里原生的 selector 的底层数据结构在返回时已经被替换成了数组。

至此,NioEventLoop 的创建已经完成了,总结一下创建 NioEventLoop 的创建过程干了哪些事。

  • 初始化了两个队列:taskQueue 和 tailQueue,类型均为 MpscQueue。taskQueue 队列是用来存放任务的队列,后面 NioEventLoop 启动后,就会循环的从这个队列中取出任务执行;tailQueue 是用来存放一些收尾工作的队列。
  • 将前面传入的ThreadPerTaskExecutor通过ThreadExecutorMap将其包装成了一个匿名类,然后保存到 NioEventLoop 的 executor 属性中,后面就能通过 NioEventLoop 来获取到线程执行器,然后执行任务了。
  • 将拒绝策略:RejectedExecutionHandlers.reject()和选择策略工厂 DefaultSelectStrategyFactory.INSTANCE 保存到 NioEventLoop 中,方便后面从 NioEventLoop 中获取。
  • 将 JDK 原生的多路复用器 Selector 保存到 NioEventLoop 的unwrappedSelector属性中,将 Netty 自定义的多路复用器 SelectedSelectionKeySetSelector 保存到 NioEventLoop 的selector属性中。unwrappedSelector 和 selector 底层的数据类型都是数组类型。

线程执行器选择工厂

当 NioEventLoop 全部创建完成后,就会接着执行第三处核心逻辑,这一步做的工作是通过一个选择工厂来创建一个线程执行器的选择器,即给 chooser 属性赋值。看到这儿,可能有点懵,什么意思呢?为什么要创建这个选择器呢?

Netty 的 NioEventLoopGroup 包含了一组线程,即一组 NioEventLoop,当有新的连接接入到服务端后,后面需要对这个新连接来进行 IO 事件的读写,那这个时候需要使用一个 NioEventLoop 来和这个新连接绑定,也就是和客户端 channel 绑定,后续对这个客户端 channel 的数据读写都是基于绑定的这个 NioEventLoop 来进行的。既然有多个 NioEventLoop 线程,那么这个时候应该从线程组中选择哪一个 NioEventLoop 来和客户端 channel 绑定呢?

Netty 的做法是:轮询,第一个客户端 channel 来了后,取线程组中的第一个线程,即 children 数组中的第一个元素;然后当第二个线程来时,取数组中的第二个元素,以此类推,循环的从 children 数组中取 NioEventLoop。这个算法很简单,如何实现呢?就是每来一个客户端 channel,先获取计数器的值,然后用计数器的值对数组取模,然后再将计数器加一。

由于取模运算相对于位运算而言,是一个相对耗时的过程,因此 netty 对此进行了优化。当线程数是 2 的整数次方时,netty 就采用位运算的方式来进行取模运算;当线程数不是 2 的整数次方时,netty 就还是采用取模的方法去进行计算。这两种计算方法分别是由两个类来实现的:PowerOfTwoEventExecutorChooser 和 GenericEventExecutorChooser,这两个类都是 EventExecutorChooser 类型,翻译过来就是事件执行器的选择器。

而 chooser 就是这两个选择器的实例,究竟是PowerOfTwoEventExecutorChooser类型的实例还是GenericEventExecutorChooser类型的实例呢,这取决于 nThread 的数量。newChooser(EventExecutor[] executors) 方法的源码如下。

public EventExecutorChooser newChooser(EventExecutor[] executors) {// executors是 new NioEventLoop() 的对象数组,// executors.length的值就是前面nThread参数的值if (isPowerOfTwo(executors.length)) {return new PowerOfTwoEventExecutorChooser(executors);} else {return new GenericEventExecutorChooser(executors);}
}

isPowerOfTwo(int val) 方法就是判断传入的值是否是 2 的整数次方。如何判断呢?又是通过位运算。下面代码可能不太直观,举个栗子:比如传入的参数是 8,那么 8 和-8 用二进制表示就是:

8:	00000000000000000000000000001000
-8:	11111111111111111111111111111000

将 8 和-8 进行与运算,结果还是 8,与原数值相等,因此 8 是 2 的整数次方。

private static boolean isPowerOfTwo(int val) {return (val & -val) == val;
}

至此,NioEventLoopGroup 的创建过程就结束了,那么 NioEventLoop 的创建过程也就跟着结束了。那么问题来了,我们说 NioEventLoop 实际上就是一个线程,既然是线程,它就必须先启动,才能轮询地执行任务,而在整个创建过程的源码中,我们都没有看到 NioEventLoop 线程启动相关的代码,那么 NioEventLoop 是什么时候启动的呢?

启动

NioEventLoop 启动的触发时机有两个,一是在服务端启动的过程中触发,另一个是在新连接接入的时候。下面以服务端启动的过程为例子,进行分析。在服务端启动过程中,会执行如下一行代码。

public ChannelFuture register(Channel channel) {return next().register(channel);
}

next() 就是 chooser 的一个方法,chooser 有两种不同的实现:PowerOfTwoEventExecutorChooser 和 GenericEventExecutorChooser,这两种不同的实现对next() 方法有不同的实现逻辑,区别就是:是用位运算从 children 数组中取出一个 NioEventLoop,还是通过取模的方式从 children 数组中取出一个 NioEventLoop,但是最终都是返回一个 NioEventLoop。

所以这儿实际上是执行NioEventLoop 的 register(channel)方法,这个方法一直向下执行,最终会执行到如下代码:

eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}
});

在这儿会调用 NioEventLoop 的 execute()方法,NioEventLoop 继承了 SingleThreadEventExecutor,execute(task)定义在 SingleThreadEventExecutor 类中,删减后的源码如下。

public void execute(Runnable task) {if (task == null) {throw new NullPointerException("task");}// 判断当前线程是否和NioEventLoop中的线程是否相等,返回true表示相等boolean inEventLoop = inEventLoop();// 将任务加入线程队列addTask(task);if (!inEventLoop) {// 启动线程startThread();// 省略部分代码...}// 省略部分代码
}

可以看到,先判断当前线程是否和 NioEventLoop 中的线程是否相等,此时由于线程是 main 线程,inEventLoop() 会返回 false,所以会进入到 if 逻辑块中,并调用startThread() 方法来启动的线程。

private void startThread() {// 处于为启动状态,才会去尝试启动线程if (state == ST_NOT_STARTED) {// 尝试将ST_NOT_STARTED设置为ST_STARTEDif (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {boolean success = false;try {doStartThread();success = true;} finally {// 如果执行doStartThread()出现异常  将STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED回滚if (!success) {STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);}}}}
}

doStart() 中,会先判断 NioEventLoop 是否处于未启动状态,只有处于未启动状态才会去尝试启动线程。在启动线程之前,会先利用 CAS 方法,将状态标识为启动状态,CAS 成功后,然后再调用doStartThread() 方法。doStartThread() 方法精简后的源码如下。

private void doStartThread() {assert thread == null;//真正的启动线程executor.execute(new Runnable() {@Overridepublic void run() {// 将此线程保存起来thread = Thread.currentThread();if (interrupted) {thread.interrupt();}boolean success = false;updateLastExecutionTime();try {// 启动NioEventLoopSingleThreadEventExecutor.this.run();success = true;} catch (Throwable t) {logger.warn("Unexpected exception from an event executor: ", t);} finally {// 省略部分代码....}}});
}

可以发现,在doStartThread() 方法中,调用的 executor 属性的execute() 方法,注意,此时 executor 属性值是什么?在创建 NioEventLoop 时,创建了一个ThreadPerTaskExecutor类型的对象,然后再通过ThreadExecutorMap将其包装成了一个匿名类,最后将这个匿名类赋值给了 executor 属性。所以此时会调用匿名类的execute(Runnable task) 方法,而这个匿名类最最终还是调用的是ThreadPerTaskExecutorexecute(Runnable task) 方法。在前面已经简单分析了ThreadPerTaskExecutorexecute(Runnable task) 方法,现在为了方便阅读,再次贴出这部分代码。

public void execute(Runnable command) {// threadFactory就是前面创建的DefaultThreadFactory// 通过线程工厂的newThread()方法来创建一个线程,并启动线程threadFactory.newThread(command).start();
}

可以看到,该execute() 方法,就是调用线程工厂的newThread(command) 方法来创建一个线程,然后调用线程的start() 的方法启动线程。当线程启动后,就会回调传入的 Runnable 任务的 run()方法,所以接着会回调到doStartThread() 方法中传入的 Runnable 的 run()方法。从doStartThread() 的源码中可以看到,在 run()方法中先将创建出来的线程保存了起来,然后会调用 SingleThreadEventExecutor.this.run() 。这一行代码就是启动 NioEventLoop 线程,该方法的源码很长,整个 NioEventLoop 的核心都在这个方法上,它实际上就是在一个无限 for 循环中,不停的去处理事件和任务。关于这个方法的源码会在下一篇文章详细分析。

至此,NioEventLoop 中的 Thread 线程已经启动了,同时会连带着 NioEventLoop 不停的在无限 for 循环中执行,也就是 NioEventLoop 启动起来了。

总结

  • 本文以new NioEventLoopGroup() 为切入点,通过分析NioEventLoopGroup的源码,从而分析了NioEventLoop的创建过程,同时还介绍了 Netty 对 NIO 的优化。接着以服务端 channel 启动的流程为入口,分析了 NioEventLoop 是如何启动的。
  • 默认情况下,netty 会创建 2 倍 CPU 核数数量的NioEventLoop线程,如果显示指定了数量,则创建指定数量的NioEventLoop
  • 最后回答下文章开头的两个问题。
  • 第一个问题:Netty 中的线程是何时启动的?启动时机有两个,一个是在服务端启动的过程中触发,另一个是在新连接接入的时候,但是最终都是调用ThreadPerTaskExecutor类的execute(Runnable command) 方法,通过线程工厂来创建一个线程,然后调用线程的start() 方法启动线程,当线程启动后,又会回调传入的 Runnable 任务的 run()方法,在任务的 run()方法中通过调用SingleThreadEventExecutor.this.run() 来调用 NioEventLoop 的 run()方法,这样就启动了 NioEventLoop。
  • 第二个问题:Netty 中的线程是如何实现串行无锁化的?从源码中我们可以知道,每个 NioEventLoop 中只包含一个线程,而每个 channel 只会绑定在一个 NioEventLoop 上,一但绑定上了,后面这个 channel 的所有 IO 操作都会交由这个 NioEventLoop 线程来处理,因此不会出现多个 NioEventLoop 线程来争夺处理 channel 的情况,因此说在 NioEventLoop 上,所有的操作都是串行处理的,不存在锁的竞争,即串行无锁化。可能有人会问,串行处理任务,岂不是降低了系统的吞吐量?显然不是的,因为 netty 中有多个 NioEventLoop 线程,多个 NioEventLoop 同时串行处理,这样服务既是多线程并行运行,各个线程间又不存在锁的竞争,大大提高了服务性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/503616.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java正则表达式匹配xml标签_用正则表达式匹配HTML\XML等文件中的标签

测试用HTML源文件&#xff1a;View Code《完美世界&#xff1a;天界的召唤》缤纷圣诞总动员[ 中华网 1小时前]经过了平安夜和圣诞节&#xff0c;节日的气氛被推到了最高点&#xff01;《完美世界&#xff1a;天界的召唤》为玩家准备了精彩纷呈的圣诞节活动&#xff0c;而玩家也…

java spring hiberate_Java程序员:Spring Boot和Hibernate一起使用的技巧

Hibernate不需要多介绍&#xff0c;它是Java中最受欢迎的ORM。同样&#xff0c;Spring Boot是功能最强大且易于使用的框架。本文并不是描述一些关于Hibernate或Spring Boot的用法&#xff0c;因为有很多。相反&#xff0c;我们将研究同时使用它们时可能遇到的一些常见错误以及如…

postgresql 查询序列_RazorSQL for Mac(数据库工具查询) v9.0.9

RazorSQL Mac激活版是一款专门为mac用户推出的数据库管理软件&#xff0c;允许您从一个数据库工具查询&#xff0c;更新&#xff0c;导航和管理所有主要数据库&#xff01;软件特色RazorSQL 是一个非开源的功能非常强大数据库查询工具、SQL的编辑、数据库管理工具。支持通过 JD…

vsm特征提取java_文本特征提取方案汇总

文本特征提取方案汇总文本分析是机器学习算法的主要应用领域。但是&#xff0c;文本分析的原始数据无法直接丢给算法&#xff0c;这些原始数据是一组符号&#xff0c;因为大多数算法期望的输入是固定长度的数值特征向量而不是不同长度的文本文件。一、文本数据的表示模型​ 文本…

脚本启动显示查询频繁被服务器防御_又被CC攻击弄得心有余悸?莫怕!这里教你如何防御...

转自CSDN&#xff0c;博主&#xff1a;一只IT小小鸟。CC攻击原理HTTP Flood 俗称CC攻击(Challenge Collapsar)是DDOS(分布式拒绝服务)的一种&#xff0c;前身名为Fatboy攻击&#xff0c;也是一种常见的网站攻击方法。是针对 Web 服务在第七层协议发起的攻击。攻击者相较其他三层…

java 怎么获取形参名_获得方法形参名称列表 -- 哦也,搞定!!

JAVA获取类的方法的参数名 – 老话题,新方法!!折腾了一天,终于搞定了.测试了nutz所有的类,均读取正常!! 完美读取任何class的变量名信息! 呵呵,当前,前提是编译时含debug信息.无任何依赖,不需要asm,不要其他任何字节码工具,纯标准JDK API实现. 核心代码,仅一个方法,130行,哦也!…

robotframework安装_python3.9.0 + robotframework + selenium3 实例体验

在win10上安装python3.9.0robotframework中我们做了基本的使用robot framework的环境搭建&#xff0c;这一章主要通过一个简单的实例来体验下robot framework的使用方式、运行、报告和日志(非常漂亮的自动化测试报告噢&#xff01;)。首先我们打开RIDE&#xff0c;快捷键 ctrln…

vmware 搭建k8s无法ping通子节点_一波四折 —— 记一次K8S集群应用故障排查

一波四折——记一次K8S集群应用故障排查Part1 初露端倪一个周四的下午&#xff0c;客户的报障打破了微信群的平静。“我们部署在自建K8S集群上的应用突然无法正常访问了&#xff0c;现在业务受到了影响&#xff01;”收到客户的报障&#xff0c;我们立刻响应&#xff0c;向客户…

python有趣的简单代码_简单几步,100行代码用Python画一个蝙蝠侠的logo

转自&#xff1a;菜鸟学Python蝙蝠侠作为DC漫画的核心人物之一&#xff0c;一直都受到广大粉丝的喜爱&#xff0c;而笔者作为DC的铁杆粉丝&#xff0c;自然也是老爷(粉丝对蝙蝠侠的昵称)的支持者。今天&#xff0c;笔者就用Python来画一个蝙蝠侠的logo&#xff0c;大概就是下图…

iis7php怎么301重定向,iis7/8设置网站301重定向的方法

准备条件&#xff1a;a、一台装有win2008以上版本的服务器 b、iis启用并且运行正常 c、在网站程序存放目录中单独创建个目录&#xff0c;目录里面留空即可(为了方便区分&#xff0c;目录名称可以设置为站点名称301&#xff0c;例如fcblog_301)1、打开Internet信息服务…

结构体内元素不确定_查漏补缺!高中三年生物最易忽略、易错的30个知识点整理不容错过...

高中生物的知识体系基本上是由大约数十个核心概念为基础构建起来的&#xff0c;这些概念包括细胞、细胞分裂、光合作用、呼吸作用、基因、染色体、遗传、变异、进化、生化系统等等&#xff0c;今天学姐来帮助你们整理一下高中三年中最容易忽略&#xff0c;也是最容易出错的30个…

基于单片机超声波测距系统的设计_一种基于UWB技术实现的测距防撞系统

叉车被广泛应用于工厂车间、仓库、流通中心和配送中心等&#xff0c;大大提高了对成件托盘货物进行装卸、堆垛和短距离运输作业的运输效率&#xff0c;几乎是所有车间必不可少的运输工具。但目前&#xff0c;简单方便的同时&#xff0c;安全事故(剐蹭、碰撞、碾压、撞车等)却也…

vb.net中递归退到最外层_数组中的逆序对

题目描述在数组中的两个数字&#xff0c;如果前面一个数字大于后面的数字&#xff0c;则这两个数字组成一个逆序对。输入一个数组&#xff0c;求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007输入描述:题目保证输入的数组中没有的相同的…

java实现条形图,JavaFX条形图

本文概述通常, 条形图可以定义为使用矩形条形表示数据的图。条的长度表示绘制在其中一根轴上的精确数值数据值。矩形条可以在图表上水平或垂直绘制。在下图中, 条形图显示了工程各个分支中的学生人数。 X轴是类别轴, 显示了不同的分支, 而Y轴是数字轴, 显示了特定分支中的学生人…

php excel 垂直居中,完美实现文字图片水平垂直居中

垂直居中是一个历史悠久的大问题&#xff0c;要做到兼容所有浏览器少不了要花点时间&#xff0c;网上也流传了很多解决方案&#xff0c;但没发现比我现在用的方案更完美&#xff0c;至少在我的项目是如此。项目中要用到垂直居中而碰到兼容性问题的&#xff0c;一般都是以下几种…

cd短是什么意思_每日命令|pwd、cd

01 命令简介上回说到《每日命令 | ls》&#xff0c;今天我们来说一说pwd命令和cd命令。pwd命令——返回当前工作目录名称。cd命令——改变工作目录。什么是工作目录&#xff1f;举个例子&#xff1a;我在北京上班&#xff0c;那我的工作地点就是北京&#xff1b;后来我到上海上…

sql 查询表结构_SQL查询语句的完整结构解析

SELECT语句完整的句法模板&#xff1a;SELECT [DISTINCT] FROM [ JOIN ON ][WHERE ][GROUP BY [HAVING ]][ORDER BY &#xff0c;...]上述句法模版中的[ ]表示该部分可选。SELECT整个语句的执行过程为&#xff1a;(1) 读取FORM子句中表、视图的数据。(2) 存在连接表时&…

基于matlab实现的云模型计算隶属度,基于MATLAB实现的云模型计算隶属度

”云”或者’云滴‘是云模型的基本单元&#xff0c;所谓云是指在其论域上的一个分布&#xff0c;可以用联合概率的形式(x&#xff0c; u)来表示云模型用三个数据来表示其特征期望&#xff1a;云滴在论域空间分布的期望&#xff0c;一般用符号Εx表示。熵&#xff1a;不确定程度…

二陈丸配什么吃不上火_宝妈一个人带孩子是什么感觉?前三种场景,不知道是怎么熬过来的...

导语&#xff1a;很多人认为一个家庭主妇很轻松&#xff0c;每天就带带孩子&#xff0c;其他什么都不需要做&#xff0c;远远没有那些人说的那么辛苦&#xff0c;无论是老公还是很多婆婆都认为是在家享福呢&#xff0c;经常就会甩出一句话“每天不就带个孩子吗&#xff1f;至于…

php怎么分割页面,将一个页面分成多个html文件(静态html分割页面)

静态html分割页面&#xff0c;达到类似PHP等动态页面的include引入页面效果。用html把首页分成三个文件web.png在PHP、JSP等动态页面开发中&#xff0c;页面里引入其它页面只需include()进来就可以实现页面的分离。如果用HTML&#xff0c;也是可以实现页面的分割的。两种方法&a…