NioEventLoopGroup 源码分析

1. 在阅读源码时做了一定的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限。为了方便 IDE 查看、跟踪、调试 代码,所以在 github 上提供 netty 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

   从今天开始,就准备进军 ne tty 了,主要的想法是看看 netty4 中一些比较重要的实现,也就是能经常出现在我们面前的东西。主要是: 线程池、通道、管道、编解码器、以及常用的工具类。

   然后现在看源码应该不会像之前的 jdk 那么细致了,主要是看了一个类以后就发现 netty 对代码封装太强了,基本一个功能可能封装了七八个类去实现,很多的抽象类但是这些抽象类中的功能还非常的多。所以说主要看这个流程,以及里面写的比较好的代码或者比较新的思想会仔细的去看看。具体的子字段,每个方法不可能做到那么细致。

   好,正式开始 netty 源码征战 !

1. 基本思路

   这里首先讲一下结论,也就是先说我看这个类的源码整理出来的思路,主要就是因为这些类太杂,一个功能在好几个类中才完全实现。

   我们在 new 一个 worker/boss 线程的时候一般是采用的直接使用的无参的构造方法,但是无参的构造方法他创建的线程池的大小是我们 CPU 核心的 2 倍。紧接着就需要 new 这么多个线程放到线程池里面,这里的线程池采用的数据结构是一个数组存放的,每一个线程需要设置一个任务队列,显然任务队列使用的是一个阻塞队列,这里实际采用的是 LinkedBlockQueue ,然后回想一下在 jdk 中的线程池是不是还有一个比较重要的参数就是线程工厂,对的!这里也有这个东西,他是需要我们手动传入的,但是如果不传则会使用一个默认的线程工厂,里面有一个 newThread 方法,这个方法实现基本和 jdk 中的实现一模一样,就是创建一个级别为 5 的非 Daemon 线程。对这就是我们在创建一个线程池时候完成的全部工作!

   好现在来具体说一下,我们每次创建的是 NioEventLoopGroup 但是他又继承了 n 个类才实现了线程池,也就是线程池的祖先是 ScheduledExecutorService 是 jdk 中的线程池的一个接口,其中里面最重要的数据结构就是一个 children 数组,用来装线程的。

   然后具体的线程他也是进行了封装的,也就是我们常看到的 NioEventLoop 。这个类里面有两个比较重要的结构:taskQueue 和 thread 。很明显这个非常类似 jdk 中的线程池。

2. NioEventLoopGroup 线程池分析

   首先要创建线程池,传入的线程数为 0,他是一直在调用 this() 最后追溯到 super(nThreads,threadFactory,selectorProvider) 也就是使用了 MultithreadEventLoopGroup 的构造方法,在这一步确定了当传入的线程数为 0 时应该设置的线程数为 CPU 核心的两倍。然后再次上调,调用了 MultithreadEventExecutorGroup 的构造方法,在这里才是真正的开始了线程池的初始化。

   首先设置了线程池工厂,然后初始化 chooser ,接着创建 n 个线程放到 children 数组中,最后设置线程中断的监听事件。

/**
* 这个方法流程:
* 1、设置了默认的线程工厂
* 2、初始化 chooser
* 3、创建nTreads个NioEventLoop对象保存在children数组中
* 4、添加中断的监听事件
* @param nThreads
* @param threadFactory
* @param args
*/
protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
} // 默认使用线程工厂是 DefaultThreadFactory
if (threadFactory == null) {
threadFactory = newDefaultThreadFactory();
} children = new SingleThreadEventExecutor[nThreads];
// 二的平方的实现是看 n&-n==n
//根据线程个数是否为2的幂次方,采用不同策略初始化chooser
if (isPowerOfTwo(children.length)) {
chooser = new PowerOfTwoEventExecutorChooser();
} else {
chooser = new GenericEventExecutorChooser();
}
//产生nTreads个NioEventLoop对象保存在children数组中
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(threadFactory, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
// 没成功,把已有的线程优雅关闭
if (!success) {
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}
// 没有完全关闭的线程让它一直等待
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
break;
}
}
}
}
} // 对每一个 children 添加中断线程时候的监听事件,就是将 terminatedChildren 自增
// 判断是否到达线程总数,是则更新 terminationFuture
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
}; for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
}

   其中有一个 if 分支用来初始化 chooser ,这个 chooser 就是用来选择使用哪个线程来执行哪些操作的。这里用到了判断一个数是否为 2 的次幂的一个方法 isPowerOfTwo() 实现比较有意思,贴出来。

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

   接下来目光要转向 newChild(threadFactory, args) ,因为在这个类里面这个方法是抽象的,在 NioEventLoopGroup 得到了实现。其实看到了也非常的简单粗暴,直接 new 了一个 NioEventLoop ,接下来就应该分析这个线程的包装类了。

    @Override
protected EventExecutor newChild(
ThreadFactory threadFactory, Object... args) throws Exception {
// 这里才是重点 也就是真正的线程 被放在自己的 children 数组中
return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0]);
}

3. NioEventLoop 线程分析

   上面已经看到了,newChild 方法就是 new 了一个 NioEventLoop 。所以有必要好好看看这个线程包装类。

   这个类的构造方法是调用了父类 SingleThreadEventLoop 的构造,接着继续上调 SingleThreadEventExecutor 构造,在这个类中才真正的实现了线程的构造。里面就做了两件事 :

  1. new 了一个新的线程,新的线程还分配了一个任务,任务的内容就是调用本类中的一个 run 方法,在 NioEventLoop 中实现。

  2. 设置任务队列为 LinkedBlockQueue

/**
* 构造方法主要完成了:
* 1、new 一个新的线程执行一个 run 方法
* 2、用 LinkedBlockQueue 初始化 taskQueue
* @param parent
* @param threadFactory
* @param addTaskWakesUp
*/
protected SingleThreadEventExecutor(EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) { if (threadFactory == null) {
throw new NullPointerException("threadFactory");
} this.parent = parent;
this.addTaskWakesUp = addTaskWakesUp;
// new 了一个新的线程
thread = threadFactory.newThread(new Runnable() {
@Override
public void run() {
boolean success = false;
updateLastExecutionTime();
try {
// 调用一个 run 方法
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
// 让线程关闭
for (;;) {
int oldState = STATE_UPDATER.get(SingleThreadEventExecutor.this);
if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
break;
}
}
// Check if confirmShutdown() was called at the end of the loop.
if (success && gracefulShutdownStartTime == 0) {
logger.error(
"Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " +
"before run() implementation terminates.");
} try {
// Run all remaining tasks and shutdown hooks.
for (;;) {
if (confirmShutdown()) {
break;
}
}
} finally {
try {
cleanup();
} finally {
STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
threadLock.release();
if (!taskQueue.isEmpty()) {
logger.warn("An event executor terminated with non-empty task queue (" + taskQueue.size() + ')');
}
terminationFuture.setSuccess(null);
}
}
}
}
}); // 使用 LinkedBlockQueue 初始化 taskQueue
taskQueue = newTaskQueue();
}

   然后看一下他要执行的 run 方法在 NioEventLoop 中得到了实现。

/**
*'wakenUp.compareAndSet(false, true)' 一般都会在 select.wakeUp() 之前执行
* 因为这样可以减少 select.wakeUp() 调用的次数,select.wakeUp() 调用是一个代价
* 很高的操作
* 注意:如果说我们过早的把 wakenUp 设置为 true,可能导致线程的竞争问题,过早设置的情形如下:
1) Selector is waken up between 'wakenUp.set(false)' and
'selector.select(...)'. (BAD)
2) Selector is waken up between 'selector.select(...)' and
'if (wakenUp.get()) { ... }'. (OK)
在第一种情况中 wakenUp 被设置为 true 则 select 会立刻被唤醒直到 wakenUp 再次被设置为 false
但是wakenUp.compareAndSet(false, true)会失败,并且导致所有希望唤醒他的线程都会失败导致
select 进行不必要的休眠 为了解决这个问题我们是在 wakenUp 为 true 的时候再次对 select 进行唤醒。
*/ @Override
protected void run() {
for (;;) {
// 获取之前的线程状态,并让 select 阻塞
boolean oldWakenUp = wakenUp.getAndSet(false);
try {
// 有任务在线程创建之后直接开始 select
if (hasTasks()) {
selectNow(); //直接调用了 select 的 selectNow 然后再次唤醒同下面的代码
// 没有任务
} else {
// 自旋进行等待可进行 select 操作
select(oldWakenUp);
// 再次唤醒,解决并发问题
if (wakenUp.get()) {
selector.wakeup();
}
} cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
// 都是处理 selected 的通道的数据,并执行所有的任务,只是在 runAllTasks 传的参数不同
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
} if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
logger.warn("Unexpected exception in the selector loop.", t);
// Prevent possible consecutive immediate failures that lead to
// excessive CPU consumption.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore.
}
}
}
}

   紧接着就是分析这个 run 方法,也就是线程在被创建之后进行的一系列操作。里面主要做了三件事:

  1. 进行 select
  2. 处理 selectedKeys
  3. 唤醒队列中所有的任务

   上面的操作都是在一个循环里面一直执行的,所以说 NioEventLoop 这个线程的作用就只有一个那就是:进行任务处理。在这个线程被 new 出来时我们就给他分配了线程的任务就是永不停歇的进行上面的操作。

   上面的过程说的是有线程安全问题,也就是如果我们过早的把 wakenUp 设置为 true,我们的 select 就会苏醒过来,而其他的线程不清楚这种状态想要设置为 wakenUp 的时候都会失败,导致 select 休眠。主要感觉有点是因为这个东西不是线程间可见的,要是采用 volatile 可能就会解决这个问题,但是 wakenUp 是 final 的不能使用 volatile 关键字修饰。所以作者采用的解决方案就是再次手动唤醒,防止由于其他线程并发设置 wakenUp 的值导致的不必要的休眠。

   然后要说一下 select 方法,这个方法的调用主要因为在队列中没有任务,所以就暂时不用 select ,这个方法里面做的就是自旋的去 select ,没有任务就 等待一段时间再去 select。

/**
* 这个方法主要干的事情:
* 1、如果不需要等待就直接 select
* 2、需要等待则等一个超时时间再去 select
* 这个过程是不停进行的也就是死循环直达有任务可进行 select 时 select 完毕退出循环
* @param oldWakenUp
* @throws IOException
*/
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
// 不用等待进行一次 select 操作
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
// 等一个超时再去选择
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++; if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// - Selected something,
// - waken up by user, or
// - the task queue has a pending task.
// - a scheduled task is ready for processing
break;
}
if (Thread.interrupted()) {
// Thread was interrupted so reset selected keys and break so we not run into a busy loop.
// As this is most likely a bug in the handler of the user or it's client library we will
// also log it.
//
// See https://github.com/netty/netty/issues/2426
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely because " +
"Thread.currentThread().interrupt() was called. Use " +
"NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
}
selectCnt = 1;
break;
} long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// timeoutMillis elapsed without anything selected.
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn(
"Selector.select() returned prematurely {} times in a row; rebuilding selector.",
selectCnt); rebuildSelector();
selector = this.selector; // Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
} currentTimeNanos = time;
} if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
}
// Harmless exception - log anyway
}
}

   接着就是 processSelectedKeys();runAllTasks(); 这两个方法,前一个方法不说就是和我们写 Nio 的时候的步骤差不多,遍历 selectedKeys 处理,然后 runAllTasks() 执行所有的任务的 run 方法。

protected boolean runAllTasks() {
fetchFromDelayedQueue();
Runnable task = pollTask();
if (task == null) {
return false;
} // 这个循环就是用来循环任务队列中的所有任务
for (;;) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception.", t);
} task = pollTask(); // 循环条件
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
return true;
}
}
}

4. 总结

   好了其实到这里线程池其实分析的已经差不多了,对于很多的细节问题并没有仔细的去看,单丝我们清楚流程以及里面的结构基本就差不多了。

   在 NioEventLoopGroup 中包装了 NioEventLoop 线程任务。具体包装在了 children 数组中,然后使用 newThread 工厂创建线程,接着给线程分配任务,任务就是进行 select 操作。

netty : NioEventLoopGroup 源码分析的更多相关文章

  1. netty之NioEventLoopGroup源码分析二

    大家好,今天我准备死磕NioEventLoopGroup的源码,首先讲下概念,NioEventLoopGroup 它是一个线程池,存放NioEventLoop,一个数组,今天打算先看下这行代码的初始化 ...

  2. Netty ByteBuf源码分析

    Netty的ByteBuf是JDK中ByteBuffer的升级版,提供了NIO buffer和byte数组的抽象视图. ByteBuf的主要类集成关系: (图片来自Netty权威指南,图中有一个画错的 ...

  3. NioEventLoopGroup源码分析与线程设定

    我的以Netty Socket编程的代码为例, 1.EventLoopGroup 进入EventLoopGroup,这是一个特殊的EventExecutorGroup,在事件循环中,在selectio ...

  4. 【Netty】源码分析目录

    前言 为方便系统的学习Netty,特整理文章目录如下. [Netty]第一个Netty应用 [Netty]Netty核心组件介绍 [Netty]Netty传输 [Netty]Netty之ByteBuf ...

  5. netty UnpooledHeapByteBuf 源码分析

    UnpooledHeapByteBuf 是基于堆内存进行内存分配的字节缓冲区,没有基于对象池技术实现,这意味着每次I/O的读写都会创建一个新的UnpooledHeapByteBuf,频繁进行大块内存的 ...

  6. NIO-EPollSelectorIpml源码分析

    目录 NIO-EPollSelectorIpml源码分析 目录 前言 初始化EPollSelectorProvider 创建EPollSelectorImpl EPollSelectorImpl结构 ...

  7. Netty源码分析第2章(NioEventLoop)---->第1节: NioEventLoopGroup之创建线程执行器

    Netty源码分析第二章: NioEventLoop 概述: 通过上一章的学习, 我们了解了Server启动的大致流程, 有很多组件与模块并没有细讲, 从这个章开始, 我们开始详细剖析netty的各个 ...

  8. Netty源码分析第2章(NioEventLoop)---->第2节: NioEventLoopGroup之NioEventLoop的创建

    Netty源码分析第二章: NioEventLoop   第二节: NioEventLoopGroup之NioEventLoop的创建 回到上一小节的MultithreadEventExecutorG ...

  9. Netty中NioEventLoopGroup的创建源码分析

    NioEventLoopGroup的无参构造: public NioEventLoopGroup() { this(0); } 调用了单参的构造: public NioEventLoopGroup(i ...

随机推荐

  1. linux下debug工具

    在linux下开发难免会遇到bug,但是由于没有图形IDE,导致debug也变得困难,其实只要掌握一些常用的debug工具,一些错误就能很快解决,本文就介绍一些常用的工具用以调试: log 输出log ...

  2. jq事件

    1,ready:当DOM载入就绪可以查询及操纵时绑定一个要执行的函数,在使用之前必须确保body元素的onload事件,,没有注册函数,否则不会触发ready函数. $(document).ready ...

  3. c#缓存技术(Dictionary)

    无论任何时候,只要传递的参数一致,返回的结果都应该是一致的.这样的函数我们才能够利用缓存.首先我们先定义一个函数,而这个函数将会是我们后面需要缓存的函数: 然后我们修改函数使之能够进行缓存: 这里我们 ...

  4. Firefox取消“订阅实时书签”功能

    如果你勾选了Firefox“总是用实时书签订阅收取”, 一打开RSS页面就直接弹出添订阅实时书签对话,而当你想在火狐中直接查看RSS页面时,发现不知如何取消“订阅实时书签”功能了.就怨这个关闭功能藏的 ...

  5. 利用XAMPP本地搭建WordPress博客

    现在越来越多的人利用WordPress搭建了自己的博客网站,我也是一样,但是还有一些人不知道怎么搭建WordPress网站的方法,因为怕弄 不好,所以也就没有花钱去做,所以这里我就讲讲怎么样利用XAM ...

  6. Eclipse中JRE(unbound)问题的一种解决方法

    (如果有写的不对的地方,欢迎指正!) 1.检查Java环境变量配置是否有问题 jdk1.8环境变量配置(1.8和8是一个意思) jdk9环境变量配置 注:配置不成功的一种可能是安装多个jdk,解决方法 ...

  7. TCP 连接关闭及TIME_WAIT探究

    这里主要记录一下TCP连接在关闭的时刻,有哪些细节问题.方便在以后的程序设计中能够注意这些细节, 以避免出现这些错误.首先我们来看一下TCP的状态转换图.如<unix网络编程>卷一所示如下 ...

  8. 员工选票系统-java

    Yuangong.java package com.toupiao; public class Yuangong { private String name; private int piao; pu ...

  9. WBS

    Need 需求分析: 为了满足中老年人因工作忙碌而无暇阅读的痛苦,我们设计推广出一款听书软件.可以给中老年人带来的好处是不再受繁琐的听书软件的束缚,操作简单,携带便捷. Approach 实现方法: ...

  10. C语言第十次博客作业--结构体

    一.PTA实验作业 题目1: 结构体数组按总分排序 1. 本题PTA提交列表 2. 设计思路 求出每名学生的总分 定义i,j循环变量 for i=0 to n for j=0 to 3 p[i].su ...