netty最核心的就是reactor线程,对应项目中使用广泛的NioEventLoop,那么NioEventLoop里面到底在干些什么事?netty是如何保证事件循环的高效轮询和任务的及时执行?又是如何来优雅地fix掉jdk的nio bug?带着这些疑问,本篇文章将庖丁解牛,带你逐步了解netty reactor线程的真相[源码基于4.1.6.Final]

reactor 线程的启动

NioEventLoop的run方法是reactor线程的主体,在第一次添加任务的时候被启动

NioEventLoop 父类 SingleThreadEventExecutor 的execute方法

  1. @Override
  2. public void execute(Runnable task) {
  3. ...
  4. boolean inEventLoop = inEventLoop();
  5. if (inEventLoop) {
  6. addTask(task);
  7. } else {
  8. startThread();
  9. addTask(task);
  10. ...
  11. }
  12. ...
  13. }

外部线程在往任务队列里面添加任务的时候执行 startThread() ,netty会判断reactor线程有没有被启动,如果没有被启动,那就启动线程再往任务队列里面添加任务

  1. private void startThread() {
  2. if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
  3. if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
  4. doStartThread();
  5. }
  6. }
  7. }

SingleThreadEventExecutor 在执行doStartThread的时候,会调用内部执行器executor的execute方法,将调用NioEventLoop的run方法的过程封装成一个runnable塞到一个线程中去执行

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

该线程就是executor创建,对应netty的reactor线程实体。executor 默认是ThreadPerTaskExecutor

默认情况下,ThreadPerTaskExecutor 在每次执行execute 方法的时候都会通过DefaultThreadFactory创建一个FastThreadLocalThread线程,而这个线程就是netty中的reactor线程实体

ThreadPerTaskExecutor

  1. public void execute(Runnable command) {
  2. threadFactory.newThread(command).start();
  3. }

关于为啥是 ThreadPerTaskExecutorDefaultThreadFactory的组合来new一个FastThreadLocalThread,这里就不再详细描述,通过下面几段代码来简单说明

标准的netty程序会调用到NioEventLoopGroup的父类MultithreadEventExecutorGroup的如下代码

  1. protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
  2. EventExecutorChooserFactory chooserFactory, Object... args) {
  3. if (executor == null) {
  4. executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
  5. }
  6. }

然后通过newChild的方式传递给NioEventLoop

  1. @Override
  2. protected EventLoop newChild(Executor executor, Object... args) throws Exception {
  3. return new NioEventLoop(this, executor, (SelectorProvider) args[0],
  4. ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
  5. }

关于reactor线程的创建和启动就先讲这么多,我们总结一下:netty的reactor线程在添加一个任务的时候被创建,该线程实体为 FastThreadLocalThread(这玩意以后会开篇文章重点讲讲),最后线程执行主体为NioEventLooprun方法。

reactor 线程的执行

那么下面我们就重点剖析一下 NioEventLoop 的run方法

  1. @Override
  2. protected void run() {
  3. for (;;) {
  4. try {
  5. switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
  6. case SelectStrategy.CONTINUE:
  7. continue;
  8. case SelectStrategy.SELECT:
  9. select(wakenUp.getAndSet(false));
  10. if (wakenUp.get()) {
  11. selector.wakeup();
  12. }
  13. default:
  14. // fallthrough
  15. }
  16. processSelectedKeys();
  17. runAllTasks(...);
  18. }
  19. } catch (Throwable t) {
  20. handleLoopException(t);
  21. }
  22. ...
  23. }

我们抽取出主干,reactor线程做的事情其实很简单,用下面一幅图就可以说明


reactor action

reactor线程大概做的事情分为对三个步骤不断循环

1.首先轮询注册到reactor线程对用的selector上的所有的channel的IO事件

  1. select(wakenUp.getAndSet(false));
  2. if (wakenUp.get()) {
  3. selector.wakeup();
  4. }

2.处理产生网络IO事件的channel

  1. processSelectedKeys();

3.处理任务队列

  1. runAllTasks(...);

下面对每个步骤详细说明

select操作

  1. select(wakenUp.getAndSet(false));
  2. if (wakenUp.get()) {
  3. selector.wakeup();
  4. }

wakenUp 表示是否应该唤醒正在阻塞的select操作,可以看到netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始,具体的select操作我们也拆分开来看

1.定时任务截止事时间快到了,中断本次轮询

  1. int selectCnt = 0;
  2. long currentTimeNanos = System.nanoTime();
  3. long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
  4. for (;;) {
  5. long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
  6. if (timeoutMillis <= 0) {
  7. if (selectCnt == 0) {
  8. selector.selectNow();
  9. selectCnt = 1;
  10. }
  11. break;
  12. }
  13. ....
  14. }

我们可以看到,NioEventLoop中reactor线程的select操作也是一个for循环,在for循环第一步中,如果发现当前的定时任务队列中有任务的截止事件快到了(<=0.5ms),就跳出循环。此外,跳出之前如果发现目前为止还没有进行过select操作(if (selectCnt == 0)),那么就调用一次selectNow(),该方法会立即返回,不会阻塞

这里说明一点,netty里面定时任务队列是按照延迟时间从小到大进行排序, delayNanos(currentTimeNanos)方法即取出第一个定时任务的延迟时间

  1. protected long delayNanos(long currentTimeNanos) {
  2. ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
  3. if (scheduledTask == null) {
  4. return SCHEDULE_PURGE_INTERVAL;
  5. }
  6. return scheduledTask.delayNanos(currentTimeNanos);
  7. }

关于netty的任务队列(包括普通任务,定时任务,tail task)相关的细节后面会另起一片文章,这里不过多展开

2.轮询过程中发现有任务加入,中断本次轮询

  1. for (;;) {
  2. // 1.定时任务截至事时间快到了,中断本次轮询
  3. ...
  4. // 2.轮询过程中发现有任务加入,中断本次轮询
  5. if (hasTasks() && wakenUp.compareAndSet(false, true)) {
  6. selector.selectNow();
  7. selectCnt = 1;
  8. break;
  9. }
  10. ....
  11. }

netty为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,就执行一次非阻塞select操作,跳出循环

3.阻塞式select操作

  1. for (;;) {
  2. // 1.定时任务截至事时间快到了,中断本次轮询
  3. ...
  4. // 2.轮询过程中发现有任务加入,中断本次轮询
  5. ...
  6. // 3.阻塞式select操作
  7. int selectedKeys = selector.select(timeoutMillis);
  8. selectCnt ++;
  9. if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
  10. break;
  11. }
  12. ....
  13. }

执行到这一步,说明netty任务队列里面队列为空,并且所有定时任务延迟时间还未到(大于0.5ms),于是,在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间

这里,我们可以问自己一个问题,如果第一个定时任务的延迟非常长,比如一个小时,那么有没有可能线程一直阻塞在select操作,当然有可能!But,只要在这段时间内,有新任务加入,该阻塞就会被释放

外部线程调用execute方法添加任务

  1. @Override
  2. public void execute(Runnable task) {
  3. ...
  4. wakeup(inEventLoop); // inEventLoop为false
  5. ...
  6. }

调用wakeup方法唤醒selector阻塞

  1. protected void wakeup(boolean inEventLoop) {
  2. if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
  3. selector.wakeup();
  4. }
  5. }

可以看到,在外部线程添加任务的时候,会调用wakeup方法来唤醒 selector.select(timeoutMillis)

阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有

  • 轮询到IO事件 (selectedKeys != 0
  • oldWakenUp 参数为true
  • 任务队列里面有任务(hasTasks
  • 第一个定时任务即将要被执行 (hasScheduledTasks()
  • 用户主动唤醒(wakenUp.get()

4.解决jdk的nio bug

关于该bug的描述见 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6595055)

该bug会导致Selector一直空轮询,最终导致cpu 100%,nio server不可用,严格意义上来说,netty没有解决jdk的bug,而是通过一种方式来巧妙地避开了这个bug,具体做法如下

  1. long currentTimeNanos = System.nanoTime();
  2. for (;;) {
  3. // 1.定时任务截止事时间快到了,中断本次轮询
  4. ...
  5. // 2.轮询过程中发现有任务加入,中断本次轮询
  6. ...
  7. // 3.阻塞式select操作
  8. selector.select(timeoutMillis);
  9. // 4.解决jdk的nio bug
  10. long time = System.nanoTime();
  11. if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
  12. selectCnt = 1;
  13. } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
  14. selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
  15. rebuildSelector();
  16. selector = this.selector;
  17. selector.selectNow();
  18. selectCnt = 1;
  19. break;
  20. }
  21. currentTimeNanos = time;
  22. ...
  23. }

netty 会在每次进行 selector.select(timeoutMillis) 之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒(这里将time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或许更好理解一些),
如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志,否则,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector

空轮询阀值相关的设置代码如下

  1. int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
  2. if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
  3. selectorAutoRebuildThreshold = 0;
  4. }
  5. SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

下面我们简单描述一下netty 通过rebuildSelector来fix空轮询bug的过程,rebuildSelector的操作其实很简单:new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上。我们抽取完主要代码之后的骨架如下

  1. public void rebuildSelector() {
  2. final Selector oldSelector = selector;
  3. final Selector newSelector;
  4. newSelector = openSelector();
  5. int nChannels = 0;
  6. try {
  7. for (;;) {
  8. for (SelectionKey key: oldSelector.keys()) {
  9. Object a = key.attachment();
  10. if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
  11. continue;
  12. }
  13. int interestOps = key.interestOps();
  14. key.cancel();
  15. SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
  16. if (a instanceof AbstractNioChannel) {
  17. ((AbstractNioChannel) a).selectionKey = newKey;
  18. }
  19. nChannels ++;
  20. }
  21. break;
  22. }
  23. } catch (ConcurrentModificationException e) {
  24. // Probably due to concurrent modification of the key set.
  25. continue;
  26. }
  27. selector = newSelector;
  28. oldSelector.close();
  29. }

首先,通过openSelector()方法创建一个新的selector,然后执行一个死循环,只要执行过程中出现过一次并发修改selectionKeys异常,就重新开始转移

具体的转移步骤为

  1. 拿到有效的key
  2. 取消该key在旧的selector上的事件注册
  3. 将该key对应的channel注册到新的selector上
  4. 重新绑定channel和新的key的关系

转移完成之后,就可以将原有的selector废弃,后面所有的轮询都是在新的selector进行

最后,我们总结reactor线程select步骤做的事情:不断地轮询是否有IO事件发生,并且在轮询的过程中不断检查是否有定时任务和普通任务,保证了netty的任务队列中的任务得到有效执行,轮询过程顺带用一个计数器避开了了jdk空轮询的bug,过程清晰明了

由于篇幅原因,下面两个过程将分别放到一篇文章中去讲述,尽请期待

process selected keys

未完待续

run tasks

未完待续

最后,通过文章开头一副图,我们再次熟悉一下netty的reactor线程做的事儿


reactor action
  1. 轮询IO事件
  2. 处理轮询到的事件
  3. 执行任务队列中的任务

netty源码分析之揭开reactor线程的面纱(一)的更多相关文章

  1. netty源码分析之揭开reactor线程的面纱(二)

    如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...

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

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

  3. Netty源码分析第2章(NioEventLoop)---->第3节: 初始化线程选择器

    Netty源码分析第二章:NioEventLoop   第三节:初始化线程选择器 回到上一小节的MultithreadEventExecutorGroup类的构造方法: protected Multi ...

  4. Netty源码分析第2章(NioEventLoop)---->第4节: NioEventLoop线程的启动

    Netty源码分析第二章: NioEventLoop   第四节: NioEventLoop线程的启动 之前的小节我们学习了NioEventLoop的创建以及线程分配器的初始化, 那么NioEvent ...

  5. Netty源码分析第8章(高性能工具类FastThreadLocal和Recycler)---->第5节: 同线程回收对象

    Netty源码分析第八章: 高性能工具类FastThreadLocal和Recycler 第五节: 同线程回收对象 上一小节剖析了从recycler中获取一个对象, 这一小节分析在创建和回收是同线程的 ...

  6. Netty源码分析第8章(高性能工具类FastThreadLocal和Recycler)---->第6节: 异线程回收对象

    Netty源码分析第八章: 高性能工具类FastThreadLocal和Recycler 第六节: 异线程回收对象 异线程回收对象, 就是创建对象和回收对象不在同一条线程的情况下, 对象回收的逻辑 我 ...

  7. Netty源码分析第8章(高性能工具类FastThreadLocal和Recycler)---->第7节: 获取异线程释放的对象

    Netty源码分析第八章: 高性能工具类FastThreadLocal和Recycler 第七节: 获取异线程释放的对象 上一小节分析了异线程回收对象, 原理是通过与stack关联的WeakOrder ...

  8. 【Netty源码分析】客户端connect服务端过程

    上一篇博客[Netty源码分析]Netty服务端bind端口过程 我们介绍了服务端绑定端口的过程,这一篇博客我们介绍一下客户端连接服务端的过程. ChannelFuture future = boos ...

  9. netty源码分析之二:accept请求

    我在前面说过了server的启动,差不多可以看到netty nio主要的东西包括了:nioEventLoop,nioMessageUnsafe,channelPipeline,channelHandl ...

随机推荐

  1. Nginx安装Nginx-echo模块

    Nginx-echo可以在Nginx中用来输出一些信息,是在测试排错过程中一个比较好的工具.它也可以做到把来自不同链接地址的信息进行一个汇总输出.总之能用起来可以给开发人员带来挺大帮助的.下面看看我们 ...

  2. Fullpage参数说明

    参数说明 $(document).ready(function() { $('#fullpage').fullpage({ //Navigation menu: false,//绑定菜单,设定的相关属 ...

  3. 如何用Python网络爬虫爬取网易云音乐歌曲

    今天小编带大家一起来利用Python爬取网易云音乐,分分钟将网站上的音乐down到本地. 跟着小编运行过代码的筒子们将网易云歌词抓取下来已经不再话下了,在抓取歌词的时候在函数中传入了歌手ID和歌曲名两 ...

  4. java实习面试题(阿里一面)

    1.抽象类和接口的不同点: 抽象类可以有构造函数,接口中不能有构造函数: 抽象类中可以有普通成员变量,但是接口中不能有普通成员变量: 抽象类中可以包含非抽象的普通方法,但是接口中必须是抽象方法:(jd ...

  5. RESTful小拓展

    RESTful 即Resource Representation State Transfer 相对应Resource 资源层,Representation 表现层,State Transfer状态转 ...

  6. 数据库导入Excel数据的简易方法

    当然,最糙猛的方式就是自己写程序读取Excel程序然后插进数据库,但那种方式要求太高.说个简单方法,主流数据库的管理工具支持CSV文件格式数据向表导入,而Excel可以另存外CSV文件,这种导入就手工 ...

  7. 关于django migrations的使用

    django 1.8之后推出的migrations机制使django的数据模式管理更方便容易,现在简单谈谈他的机制和一些问题的解决方法: 1.谈谈机制:migrations机制有两个指令,第一个是ma ...

  8. JDBC连接数据库时候出错

    错误提示如下: Fri May 13 09:06:04 CST 2016 WARN: Establishing SSL connection without server's identity ver ...

  9. webService(一)开篇

    Webservice技术在web开发中算是一个比较常见技术.这个对于大多数的web开发者,别管是Java程序员还是.NET程序员应该都不是很陌生.今天我就和大家一起来学习一下webservice的基本 ...

  10. NewLife.Net——网络压测单机1.88亿tps

    NewLife.Net压力测试,峰值4.2Gbps,50万pps,消息大小24字节,消息处理速度1.88亿tps! 共集合20台高配ECS参与测试,主服务器带宽6Gbps.100万pps,16核心64 ...