Netty 学习(七):NioEventLoop 对应线程的创建和启动源码说明

作者: Grey

原文地址:

博客园:Netty 学习(七):NioEventLoop 对应线程的创建和启动源码说明

CSDN:Netty 学习(七):NioEventLoop 对应线程的创建和启动源码说明

说明

在 Netty 服务端代码中,我们一般会创建了两个 NioEventLoopGroup:bossGroup 和 workerGroup

其中: bossGroup用于监听端口,接收新连接的线程组;workerGroup 用于处理每一个连接的数据读写的线程组。

bossGroup 创建第一个 NioEventLoop 线程

NioEventLoop 的启动入口在AbstractUnsafe

  1. @Override
  2. public final void register(EventLoop eventLoop, final ChannelPromise promise) {
  3. ......
  4. AbstractChannel.this.eventLoop = eventLoop;
  5. if (eventLoop.inEventLoop()) {
  6. register0(promise);
  7. } else {
  8. try {
  9. eventLoop.execute(new Runnable() {
  10. @Override
  11. public void run() {
  12. register0(promise);
  13. }
  14. });
  15. } catch (Throwable t) {
  16. logger.warn(
  17. "Force-closing a channel whose registration task was not accepted by an event loop: {}",
  18. AbstractChannel.this, t);
  19. closeForcibly();
  20. closeFuture.setClosed();
  21. safeSetFailure(promise, t);
  22. }
  23. }
  24. }

其中inEventLoop()方法调用的是AbstractEventExecutor的实现

  1. @Override
  2. public boolean inEventLoop() {
  3. return inEventLoop(Thread.currentThread());
  4. }

而这个实现又调用了子类SingleThreadEventExecutor的如下方法

  1. @Override
  2. public boolean inEventLoop(Thread thread) {
  3. return thread == this.thread;
  4. }

在服务端刚启动的时候,Thread.currentThread()就是当前 main 方法对应的主线程,而this.thread还没有开始赋值,所以此时为null,

所以eventLoop.inEventLoop()在一开始调用的时候,返回的是 false,进入AbstractUnsafe的如下else逻辑中

  1. @Override
  2. public final void register(EventLoop eventLoop, final ChannelPromise promise) {
  3. ......
  4. AbstractChannel.this.eventLoop = eventLoop;
  5. // 首次执行的时候 eventLoop.inEventLoop() 返回 false,执行 else 逻辑
  6. if (eventLoop.inEventLoop()) {
  7. ......
  8. } else {
  9. ......
  10. eventLoop.execute(new Runnable() {
  11. @Override
  12. public void run() {
  13. register0(promise);
  14. }
  15. });
  16. ......
  17. }
  18. }

其中executor方法对应的是SingleThreadEventExecutorexecute方法

  1. private void execute(Runnable task, boolean immediate) {
  2. boolean inEventLoop = inEventLoop();
  3. addTask(task);
  4. if (!inEventLoop) {
  5. startThread();
  6. if (isShutdown()) {
  7. ......
  8. }
  9. }
  10. if (!addTaskWakesUp && immediate) {
  11. ......
  12. }
  13. }

inEventLoop()经过上述分析,为false,所以执行startThread()方法

  1. private void startThread() {
  2. if (state == ST_NOT_STARTED) {
  3. if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
  4. boolean success = false;
  5. try {
  6. doStartThread();
  7. success = true;
  8. } finally {
  9. if (!success) {
  10. STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
  11. }
  12. }
  13. }
  14. }
  15. }

这里主要的逻辑就是判断线程是否启动,如果没有启动,就调用doStartThread()启动。doStartThread()的逻辑是

  1. private void doStartThread() {
  2. assert thread == null;
  3. executor.execute(new Runnable() {
  4. @Override
  5. public void run() {
  6. thread = Thread.currentThread();
  7. ...
  8. SingleThreadEventExecutor.this.run();
  9. ......
  10. }
  11. });
  12. }

通过一个成员变量thread来保存ThreadPerTaskExecutor创建出来的线程(即:FastThreadLocalThread),NioEventLoop 保存完线程的引用之后,随即调用 run 方法。

workGroup 对应的 NioEventLoop 创建线程和启动

workGroup 对应的 NioEventLoop 创建的线程主要做如下事情

  1. 执行一次事件轮询。首先轮询注册到 Reactor 线程对应的 Selector 上的所有 Channel 的 IO 事件。

  2. 处理产生 IO 事件的 Channel。如果有读写或者新连接接入事件,则处理:

  3. 处理任务队列。

以上三个步骤分别对应了下述三个方法

事件轮询

事件轮询调用了NioEventLoop的如下方法

  1. private int select(long deadlineNanos) throws IOException {
  2. if (deadlineNanos == NONE) {
  3. return selector.select();
  4. }
  5. // Timeout will only be 0 if deadline is within 5 microsecs
  6. long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
  7. return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
  8. }

处理 IO 事件的 Channel

调用的是NioEventLoop的如下方法

  1. private void processSelectedKeys() {
  2. if (selectedKeys != null) {
  3. // 处理优化过的 SelectedKeys
  4. processSelectedKeysOptimized();
  5. } else {
  6. // 处理正常的 SelectedKeys
  7. processSelectedKeysPlain(selector.selectedKeys());
  8. }
  9. }

上述两个分支分别处理了不同类型的 key:重点关注优化过的 SelectedKeys,selectedKeys 在 NioEventLoop 中是一个SelectedSelectionKeySet对象,这个对象虽然叫Set,但是底层使用了数组

  1. final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
  2. SelectionKey[] keys;
  3. int size;
  4. SelectedSelectionKeySet() {
  5. keys = new SelectionKey[1024];
  6. }
  7. @Override
  8. public boolean add(SelectionKey o) {
  9. if (o == null) {
  10. return false;
  11. }
  12. keys[size++] = o;
  13. if (size == keys.length) {
  14. increaseCapacity();
  15. }
  16. return true;
  17. }
  18. ......
  19. }

add 方法的主要流程是:

  1. 将SelectionKey塞到该数组的尾部;

  2. 更新该数组的逻辑长度+1;

  3. 如果该数组的逻辑长度等于数组的物理长度,就将该数组扩容。

待程序运行一段时间后,等数组的长度足够长,每次在轮询到 NIO 事件的时候,Netty 只需要O(1)的时间复杂度就能将SelectionKey塞到set中去,而 JDK 底层使用的HashSet,put的时间复杂度最少是O(1),最差是O(n)。

进入processSelectedKeysOptimized方法

  1. private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
  2. for (int i = 0;; i ++) {
  3. final SelectionKey k = selectedKeys[i];
  4. if (k == null) {
  5. break;
  6. }
  7. // null out entry in the array to allow to have it GC'ed once the Channel close
  8. // See https://github.com/netty/netty/issues/2363
  9. selectedKeys[i] = null;
  10. final Object a = k.attachment();
  11. if (a instanceof AbstractNioChannel) {
  12. processSelectedKey(k, (AbstractNioChannel) a);
  13. } else {
  14. @SuppressWarnings("unchecked")
  15. NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
  16. processSelectedKey(k, task);
  17. }
  18. if (needsToSelectAgain) {
  19. // null out entries in the array to allow to have it GC'ed once the Channel close
  20. // See https://github.com/netty/netty/issues/2363
  21. for (;;) {
  22. i++;
  23. if (selectedKeys[i] == null) {
  24. break;
  25. }
  26. selectedKeys[i] = null;
  27. }
  28. selectAgain();
  29. // Need to flip the optimized selectedKeys to get the right reference to the array
  30. // and reset the index to -1 which will then set to 0 on the for loop
  31. // to start over again.
  32. //
  33. // See https://github.com/netty/netty/issues/1523
  34. selectedKeys = this.selectedKeys.flip();
  35. i = -1;
  36. }
  37. }
  38. }

主要是三个步骤:

第一步,取出 IO 事件及对应的 Channel。其中selectedKeys[i] = null;的目的是防止内存泄漏

第二步,处理 Channel

  1. if (a instanceof AbstractNioChannel) {
  2. processSelectedKey(k, (AbstractNioChannel) a);
  3. } else {
  4. @SuppressWarnings("unchecked")
  5. NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
  6. processSelectedKey(k, task);
  7. }

Netty 的轮询注册机制其实是将 AbstractNioChannel 内部的 JDK 类 SelectableChannel 对象注册到 JDK 类 Selector 对象上,并且将 AbstractNioChannel 作为SelectableChannel 对象的一个 attachment 附属上,这样在 JDK 轮询出某条 SelectableChannel 有 IO 事件发生时,就可以直接取出 AbstractNioChannel 进行后续操作。

在Netty的Channel中,有两大类型的Channel,

一个是NioServerSocketChannel,由boss NioEventLoopGroup负责处理;

一个是NioSocketChannel,由worker NioEventLoop负责处理,

所以:

(1)对于boss NioEventLoop来说,轮询到的是连接事件,后续通过NioServerSocketChannel的Pipeline将连接交给一个worker NioEventLoop处理;

(2)对于worker NioEventLoop来说,轮询到的是读写事件,后续通过NioSocketChannel的Pipeline将读取到的数据传递给每个ChannelHandler来处理。

第三步,判断是否需要再一次轮询

needsToSelectAgain变量控制,needsToSelectAgain变量在如下方法中被调用,在NioEventLoop

  1. private static final int CLEANUP_INTERVAL = 256;
  2. void cancel(SelectionKey key) {
  3. key.cancel();
  4. cancelledKeys ++;
  5. if (cancelledKeys >= CLEANUP_INTERVAL) {
  6. cancelledKeys = 0;
  7. needsToSelectAgain = true;
  8. }
  9. }

cancel方法是用于将key取消,并且在被取消的key到达CLEANUP_INTERVAL的时候,设置needsToSelectAgain为 true,CLEANUP_INTERVAL默认值为256。

也就是说,对于每个NioEventLoop而言,每隔256个Channel从Selector上移除的时候,就标记needsToSelectAgain为true,然后将SelectedKeys的内部数组全部清空,方便JVM垃圾回收,然后调用selectAgain重新填装SelectionKeys数组。

处理任务队列

调用的是如下方法

  1. protected boolean runAllTasks() {
  2. assert inEventLoop();
  3. boolean fetchedAll;
  4. boolean ranAtLeastOne = false;
  5. do {
  6. fetchedAll = fetchFromScheduledTaskQueue();
  7. if (runAllTasksFrom(taskQueue)) {
  8. ranAtLeastOne = true;
  9. }
  10. } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
  11. if (ranAtLeastOne) {
  12. lastExecutionTime = getCurrentTimeNanos();
  13. }
  14. afterRunningAllTasks();
  15. return ranAtLeastOne;
  16. }

主要流程如下:

1.NioEventLoop在执行过程中不断检测是否有事件发生,如果有事件发生就处理,处理完事件之后再处理外部线程提交过来的异步任务。

2.在检测是否有事件发生的时候,为了保证异步任务的及时处理,只要有任务要处理,就立即停止事件检测,随即处理任务。

3.外部线程异步执行的任务分为两种:定时任务和普通任务,分别落地到 MpscQueue 和 PriorityQueue ,而 PriorityQueue 中的任务最终都会填充到MpscQueue中处理。

4.Netty每隔64个任务检查一次是否该退出任务循环。

完整代码见:hello-netty

本文所有图例见:processon: Netty学习笔记

更多内容见:Netty专栏

参考资料

跟闪电侠学 Netty:Netty 即时聊天实战与底层原理

深度解析Netty源码

Netty 学习(七):NioEventLoop 对应线程的创建和启动源码说明的更多相关文章

  1. Netty学习:ChannelHandler执行顺序详解,附源码分析

    近日学习Netty,在看书和实践的时候对于书上只言片语的那些话不是十分懂,导致尝试写例子的时候遭遇各种不顺,比如decoder和encoder还有HttpObjectAggregator的添加顺序,研 ...

  2. JDBC线程池创建与DBCP源码阅读

    创建数据库连接是一个比较消耗性能的操作,同时在并发量较大的情况下创建过多的连接对服务器形成巨大的压力.对于资源的频繁分配﹑释放所造成的问题,使用连接池技术是一种比较好的解决方式. 在Java中,连接池 ...

  3. Java线程:创建与启动

    Java线程:创建与启动 一.定义线程   1.扩展java.lang.Thread类.   此类中有个run()方法,应该注意其用法: public void run() 如果该线程是使用独立的 R ...

  4. 03_线程的创建和启动_实现Runnable接口方式

    [线程的创建和启动的步骤(实现Runnable接口方式)] 1.定义Runnable接口的实现类,并重写其中的run方法.run()方法的方法体是线程执行体. class SonThread  imp ...

  5. 线程池 ThreadPoolExecutor 原理及源码笔记

    前言 前面在学习 JUC 源码时,很多代码举例中都使用了线程池 ThreadPoolExecutor,并且在工作中也经常用到线程池,所以现在就一步一步看看,线程池的源码,了解其背后的核心原理. 公众号 ...

  6. 【转载】深度解读 java 线程池设计思想及源码实现

    总览 开篇来一些废话.下图是 java 线程池几个相关类的继承结构: 先简单说说这个继承结构,Executor 位于最顶层,也是最简单的,就一个 execute(Runnable runnable) ...

  7. 线程池 ThreadPoolExecutor 类的源码解析

    线程池 ThreadPoolExecutor 类的源码解析: 1:数据结构的分析: private final BlockingQueue<Runnable> workQueue;  // ...

  8. Java并发指南12:深度解读 java 线程池设计思想及源码实现

    ​深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...

  9. Maven自定义绑定插件目标:创建项目的源码jar

    <build> <plugins> <!-- 自定义绑定,创建项目的源码jar --> <plugin> <groupId>org.apac ...

随机推荐

  1. Centos7 安装mysql服务器并开启远程访问功能

    大二的暑假,波波老师送了一个华为云的服务器给我作测试用,这是我程序员生涯里第一次以root身份拥有一台真实的云服务器 而之前学习的linux知识在这时也派上了用场,自己的物理机用的是ubuntu系统, ...

  2. VMware虚拟机安装基于Debian的统信UOS系统

    统信操作系统(UOS)是一款美观易用.安全可靠的国产桌面操作系统.UOS预装了Google Chrome.WPS Office.搜狗输入法以及一系列原生应用.它既能让您体验到丰富多彩的娱乐生活,也可以 ...

  3. gotoscan:CMS指纹识别工具

    gotoscan 前言 项目地址 https://github.com/newbe3three/gotoscan 结合自己学习到的Go相关知识,通过实现这个简易的CMS指纹识别工具来锻炼一下自己写代码 ...

  4. Go语言基础四:数组和指针

    GO语言中数组和指针 数组 Go语言提供了数组类型的数据结构. 数组是同一数据类型元素的集合.这里的数据类型可以是整型.字符串等任意原始的数据类型.数组中不允许混合不同类型的元素.(当然,如果是int ...

  5. 使用Django2.0.4集成钉钉第三方扫码登录

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_124 钉钉作为阿里旗下的一款免费移动通讯软件,受众群体越来越多,这里我们使用Django来集成一下钉钉的三方账号登录,首先注册钉钉 ...

  6. Linux 系统时间同步服务器配置

    # Linux 时间同步 # 查看系统时间: date # 查看硬件日期 # ntp 软件 # chrony 软件 chrony比ntp更精确 # 利用ntp手动瞬间同步时间: ntpdate 172 ...

  7. #万答10:mysqldump 是如何实现一致性备份的

    万答10:mysqldump 是如何实现一致性备份的 实验场景 MySQL 8.0.25 InnoDB 实验步骤: 先开启 general_log 观察导出执行过程的变化 set global gen ...

  8. 查找默认安装的python路径,并输出到 FindPythonPathX_output.txt

    在python程序设计教学中,在汉化IDEL时.为PyCharm项目设置解释器时,经常需要查找python安装路径.对老手来说很简单,但对很多刚开始学习编程的学生来说,则很困难.所以,编写了一个批处理 ...

  9. SVN:取消对代码的修改

    取消对代码的修改分为两种情况: 第一种情况:改动没有被提交(commit). 这种情况下,使用svnrevert就能取消之前的修改. svn revert用法如下: #svn revert[-R] s ...

  10. Spring 02 控制反转

    简介 IOC IOC(Inversion of Control),即控制反转. 这不是一项技术,而是一种思想. 其根本就是对象创建的控制权由使用它的对象转变为第三方的容器,即控制权的反转. DI DI ...