EventLoopGroup 与Reactor:

  前面的章节中我们已经知道了,一个Netty 程序启动时,至少要指定一个EventLoopGroup(如果使用到的是NIO,通常是指NioEventLoopGroup),那么,这个NioEventLoopGroup 在Netty 中到底扮演着什么角色呢? 我们知道,Netty是Reactor 模型的一个实现,我们就从Reactor 的线程模型开始。

浅谈Reactor 线程模型:

  Reactor 的线程模型有三种:单线程模型、多线程模型、主从多线程模型。首先来看一下单线程模型,如下图所示:

  所谓单线程, 即Acceptor 处理和andler 处理都在同一个线程中处理。这个模型的坏处显而易见:当其中某个Handler阻塞时, 会导致其他所有的Client 的Handler 都得不到执行,并且更严重的是,Handler 的阻塞也会导致整个服务不能接收新的Client 请求(因为Acceptor 也被阻塞了)。因为有这么多的缺陷,因此单线程Reactor 模型应用场景比较少。

  那么,什么是多线程模型呢? Reactor 的多线程模型与单线程模型的区别就是Acceptor 是一个单独的线程处理,并且有一组特定的NIO 线程来负责各个客户端连接的IO 操作。Reactor 多线程模型如下图所示:

Reactor 多线程模型有如下特点:

  1. 有专门一个线程,即Acceptor 线程用于监听客户端的TCP 连接请求。
  2. 客户端连接的IO 操作都由一个特定的NIO 线程池负责.每个客户端连接都与一个特定的NIO 线程绑定,因此在这个客户端连接中的所有IO 操作都是在同一个线程中完成的。
  3. 客户端连接有很多,但是NIO 线程数是比较少的,因此一个NIO 线程可以同时绑定到多个客户端连接中。

  接下来我们再来看一下Reactor 的主从多线程模型。一般情况下, Reactor 的多线程模式已经可以很好的工作了,但是我们想象一个这样的场景:如果我们的服务器需要同时处理大量的客户端连接请求或我们需要在客户端连接时,进行一些权限的校验,那么单线程的Acceptor 很有可能就处理不过来,造成了大量的客户端不能连接到服务器。Reactor 的主从多线程模型就是在这样的情况下提出来的,它的特点是:服务器端接收客户端的连接请求不再是一个线程,而是由一个独立的线程池组成。其线程模型如下图所示:

  可以看到,Reactor 的主从多线程模型和Reactor 多线程模型很类似,只不过Reactor 的主从多线程模型的Acceptor使用了线程池来处理大量的客户端请求。

EventLoopGroup 与Reactor 关联:

  我们介绍了三种Reactor 的线程模型, 那么它们和NioEventLoopGroup 又有什么关系呢? 其实, 不同的设置NioEventLoopGroup 的方式就对应了不同的Reactor 的线程模型。

1、单线程模型,来看下面的应用代码:

EventLoopGroup bossGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup);

  注意,我们实例化了一个NioEventLoopGroup,然后接着我们调用server.group(bossGroup)设置了服务器端的EventLoopGroup。有人可能会有疑惑;我记得在启动服务器端的Netty 程序时, 需要设置bossGroup 和workerGroup,为何这里只设置一个bossGroup?其实原因很简单,ServerBootstrap 重写了group 方法:

public ServerBootstrap group(EventLoopGroup group) {
return group(group, group);
}

  因此当传入一个group 时,那么bossGroup 和workerGroup 就是同一个NioEventLoopGroup 了。这时,因为bossGroup 和workerGroup 就是同一个NioEventLoopGroup,并且这个NioEventLoopGroup 线程池数量只设置了1个线程,也就是说Netty 中的Acceptor 和后续的所有客户端连接的IO 操作都是在一个线程中处理的。那么对应到Reactor 的线程模型中,我们这样设置NioEventLoopGroup 时,就相当于Reactor 的单线程模型。

2、多线程模型,再来看下面的应用代码:

EventLoopGroup bossGroup = new NioEventLoopGroup(128);
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup);

  从上面代码中可以看出,我们只需要将bossGroup 的参数就设置为大于1 的数,其实就是Reactor 多线程模型。

3、主从线程模型,到这里相信大家都已经想到了, 实现主从线程模型的代码如下:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);

  bossGroup 为主线程,而workerGroup 中的线程是CPU 核心数乘以2,因此对应的到Reactor 线程模型中,我们知道, 这样设置的NioEventLoopGroup 其实就是Reactor 主从多线程模型。

EventLoopGroup 的实例化:

  首先,我们先纵览一下EventLoopGroup 的类结构图,如下图所示:

  在前面的章节中我们已经简单地介绍了一下NioEventLoopGroup 初始化的基本过程,这里我们再回顾一下时序图:

基本步骤如下:

  1. EventLoopGroup(其实是MultithreadEventExecutorGroup)内部维护一个类为EventExecutor children 数组,其大小是nThreads,这样就初始化了一个线程池。
  2. 如果我们在实例化NioEventLoopGroup 时,如果指定线程池大小,则nThreads 就是指定的值,否则是CPU核数* 2。
  3. 在MultithreadEventExecutorGroup 中会调用newChild()抽象方法来初始化children 数组.
  4. 抽象方法newChild()实际是在NioEventLoopGroup 中实现的,由它返回一个NioEventLoop 实例。
  5. 初始化NioEventLoop 主要属性:
    1. provider:在NioEventLoopGroup 构造器中通过SelectorProvider 的provider()方法获取SelectorProvider。
    2. selector:在NioEventLoop 构造器中调用selector = provider.openSelector()方法获取Selector 对象。

任务执行者EventLoop:

  NioEventLoop 继承自SingleThreadEventLoop,而SingleThreadEventLoop 又继承自SingleThreadEventExecutor。而SingleThreadEventExecutor 是Netty 中对本地线程的抽象,它内部有一个Thread thread 属性,存储了一个本地Java线程。因此我们可以简单地认为,一个NioEventLoop 其实就是和一个特定的线程绑定,并且在其生命周期内,绑定的线程都不会再改变。

  NioEventLoop 的类层次结构图还是有些复杂的,不过我们只需要关注几个重要点即可。首先来看NioEventLoop 的继承链:NioEventLoop->SingleThreadEventLoop->SingleThreadEventExecutor->AbstractScheduledEventExecutor。在AbstractScheduledEventExecutor 中, Netty 实现了NioEventLoop 的schedule 功能,即我们可以通过调用一个NioEventLoop 实例的schedule 方法来运行一些定时任务。而在SingleThreadEventLoop 中,又实现了任务队列的功能,通过它,我们可以调用一个NioEventLoop 实例的execute()方法来向任务队列中添加一个task,并由NioEventLoop进行调度执行。通常来说,NioEventLoop 负责执行两个任务:第一个任务是作为IO 线程,执行与Channel 相关的IO 操作,包括调用Selector 等待就绪的IO 事件、读写数据与数据的处理等;而第二个任务是作为任务队列,执行taskQueue 中的任务,例如用户调用eventLoop.schedule 提交的定时任务也是这个线程执行的。

NioEventLoop 的实例化过程:

  之前的章节我们分析过,SingleThreadEventExecutor 启动时会调用doStartThread()方法,然后调用executor.execute()方法,将当前线程赋值给thread。在这个线程中所做的事情主要就是调用SingleThreadEventExecutor.this.run()方法,而因为NioEventLoop 实现了这个方法,因此根据多态性,其实调用的是NioEventLoop.run()方法。

EventLoop 与Channel 的关联:

  在Netty 中, 每个Channel 都有且仅有一个EventLoop 与之关联, 它们的关联过程如下(红色部分):

  从上图中我们可以看到,当调用AbstractChannel$AbstractUnsafe.register()方法后,就完成了Channel 和EventLoop的关联。在AbstractChannel$AbstractUnsafe.register() 方法中, 会将一个EventLoop 赋值给AbstractChannel 内部的eventLoop 字段,这句代码就是完成EventLoop 与Channel 的关联过程。

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
AbstractChannel.this.eventLoop = eventLoop;
register0(promise); }

EventLoop 的启动:

  在前面我们已经知道了,NioEventLoop 本身就是一个SingleThreadEventExecutor,因此NioEventLoop 的启动,其实就是NioEventLoop 所绑定的本地Java 线程的启动。按照这个思路,我们只需要找到在哪里调用了SingleThreadEventExecutor 中thread 字段的start()方法就可以知道是在哪里启动的这个线程了。从前面章节的分析中, 其实我们已经清楚: thread.start() 被封装到了SingleThreadEventExecutor.startThread()方法中,来看代码:

private void startThread() {
if (STATE_UPDATER.get(this) == && STATE_UPDATER.compareAndSet(this, , )) {
this.doStartThread();
}
}

  STATE_UPDATER 是SingleThreadEventExecutor 内部维护的一个属性,它的作用是标识当前的thread 的状态。在初始的时候,STATE_UPDATER == ST_NOT_STARTED,因此第一次调用startThread()方法时,就会进入到if 语句内,进而调用到thread.start()方法。我们发现,startThread 是在SingleThreadEventExecutor 的execute()方法中调用的。既然如此,那现在我们的工作就变为了寻找在哪里第一次调用了SingleThreadEventExecutor 的execute()方法。

  如果细心的小伙伴可能已想到了, 我们在前面章节中, 我们有提到到在注册channel 的过程中, 会在AbstractChannel$AbstractUnsafe 的register()中调用eventLoop.execute()方法,在EventLoop 中进行Channel 注册代码的执行,AbstractChannel$AbstractUnsafe 的register()部分代码如下:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
}
}

  很显然,一路从Bootstrap 的bind()方法跟踪到AbstractChannel$AbstractUnsafe 的register()方法,整个代码都是在主线程中运行的,因此上面的eventLoop.inEventLoop()返回为false,于是进入到else 分支,在这个分支中调用了eventLoop.execute()方法,而NioEventLoop 没有实现execute()方法,因此调用的是SingleThreadEventExecutor 的execute()方法:

public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
} else {
boolean inEventLoop = this.inEventLoop();
if (inEventLoop) {
this.addTask(task);
} else {
this.startThread();
this.addTask(task);
if (this.isShutdown() && this.removeTask(task)) {
reject();
}
} if (!this.addTaskWakesUp && this.wakesUpForTask(task)) {
this.wakeup(inEventLoop);
} }
}

  我们已经分析过了, inEventLoop == false , 因此执行到else 分支, 在这里就调用startThread() 方法来启动SingleThreadEventExecutor 内部关联的Java 本地线程了。

  总结一句话:当EventLoop 的execute()第一次被调用时,就会触发startThread()方法的调用,进而导致EventLoop所对应的Java 本地线程启动。

Netty之大名鼎鼎的EventLoop的更多相关文章

  1. Netty实战七之EventLoop和线程模型

    简单地说,线程模型指定了操作系统.编程语言.框架或者应用程序的上下文中的线程管理的关键方面.Netty的线程模型强大但又易用,并且和Netty的一贯宗旨一样,旨在简化你的应用程序代码,同时最大限度地提 ...

  2. Netty 源码学习——EventLoop

    Netty 源码学习--EventLoop 在前面 Netty 源码学习--客户端流程分析中我们已经知道了一个 EventLoop 大概的流程,这一章我们来详细的看一看. NioEventLoopGr ...

  3. netty中的发动机--EventLoop及其实现类NioEventLoop的源码分析

    EventLoop 在之前介绍Bootstrap的初始化以及启动过程时,我们多次接触了NioEventLoopGroup这个类,关于这个类的理解,还需要了解netty的线程模型.NioEventLoo ...

  4. Netty源码死磕一(netty线程模型及EventLoop机制)

    引言 好久没有写博客了,近期准备把Netty源码啃一遍.在这之前本想直接看源码,但是看到后面发现其实效率不高, 有些概念还是有必要回头再细啃的,特别是其线程模型以及EventLoop的概念. 当然在开 ...

  5. [编织消息框架][netty源码分析]4 eventLoop 实现类NioEventLoop职责与实现

    NioEventLoop 是jdk nio多路处理实现同修复jdk nio的bug 1.NioEventLoop继承SingleThreadEventLoop 重用单线程处理 2.NioEventLo ...

  6. EventLoop(netty源码死磕4)

    精进篇:netty源码  死磕4-EventLoop的鬼斧神工 目录 1. EventLoop的鬼斧神工 2. 初识 EventLoop 3. Reactor模式回顾 3.1. Reactor模式的组 ...

  7. 从Netty EventLoop实现上可以学到什么

    本文主要讨论Netty NioEventLoop原理及实践,关于Netty NioEventLoop,首先要知道NioEventLoop是什么,为什么它会是Netty核心Reactor处理器,实现原理 ...

  8. Netty学习:EventLoop事件机制

    目录 EventLoop是什么 EventLoop适用的场景 Netty中的EventLoop Netty中的大量inEventLoop判断 Netty是如何建立连接并监听端口的-NIOSocketC ...

  9. Netty 框架学习 —— EventLoop 和线程模型

    EventLoop 接口 Netty 是基于 Java NIO 的,因此 Channel 也有其生命周期,处理一个连接在其生命周期内发生的事件是所有网络框架的基本功能.通常来说,我们使用一个线程来处理 ...

随机推荐

  1. VUE:v-for获取列表前n个数据、中间范围数据、末尾n条数据的方法

    说明: 1.开发使用的UI是mintUI, 要求: 1.获取6到13之间的数据:items.slice(6,13) <mt-cell v-for="(item,index) in it ...

  2. Spark集成Kafka实时流计算Java案例

    package com.test; import java.util.*; import org.apache.spark.SparkConf; import org.apache.spark.Tas ...

  3. apache重写规则简单理解

    1.前提:开启apache重写,并把httpd.conf里的相关的AllowOverride denied改为AllowOverride all 2.重写规则可写在项目根目录的.htaccess文件或 ...

  4. 封装操作mysql、redis

    封装操作mysql: import pymysql class MyDb: def __init__(self,host,password,user,db,port=3306,charset='utf ...

  5. python字符串前面的u,还有r

    以u或U开头的字符串表示unicode字符串 如果你想要用非英语写文本,那么你需要有一个支持Unicode的编辑器.(了解一下unicode和ascll码还有utf-8) u'你好'        # ...

  6. CF429E Points and Segments

    链接 CF429E Points and Segments 给定\(n\)条线段,然后给这些线段红蓝染色,求最后直线上上任意一个点被蓝色及红色线段覆盖次数之差的绝对值不大于\(1\),构造方案,\(n ...

  7. 网络安全专家教你设置史上最安全的WiFi密码

    通过设置强密码可以防止WiFi被蹭网现象的发生,保证WiFi网络安全.那么我们的WiFi密码怎么设置才最安全呢? 提供以下设置建议: 1.WiFi密码设置尽量使用字母.数字和字符组成的密码.这种密码强 ...

  8. ORM多表操作上

    一.创建模型 例:我们来假定下面这些概念,字段和关系 作者模型:一个作者有姓名和年龄. 作者详细模型:把作者的详情放到详情表,包含生日,手机号,家庭住址等信息.作者详情模型和作者模型之间是一对一(on ...

  9. tensorflow图像处理函数(1)

    1.tensorflow中对jpeg格式图像的编码/解码函数: import matplotlib.pyplot as plt import tensorflow as tf image_raw_da ...

  10. Oracle常用基础语法(未完待补和操作)

    这篇博客主要是Oracle常用基础语法, 另外,存储过程和存储函数很重要,这个后期看视频学习,还有DB优化,另外,还有plsql develop和navicat的使用,重点是数据的导入导出: ---- ...