扫描下方二维码或者微信搜索公众号
菜鸟飞呀飞
,即可关注微信公众号,阅读更多Spring源码分析
和Java并发编程
文章。
1.前言
在上一篇文章中分析了NioEventLoop的创建以及启动过程的源码,在文章结尾处提到,当NioEventLoop线程启动以后,会一直在一个无限 for 循环中一直循环,至死方休,那么在循环中,NioEventLoop到底在循环处理什么呢?这将是本文分析的重点。同时,还是先思考一下以下两个问题。
- 众所周知,Netty 巧妙的避免了 JDK 的空轮询的 BUG,那么什么是 JDK 的空轮询 BUG?
- Netty 又是如何避免的?
2.NioEventLoop.run()
在 NioEventLoop 通过 doStartThread()启动线程后,会执行这一行代码。这一行代码最终会调用的是 NioEventLoop 的 run()方法。
SingleThreadEventExecutor.this.run();
run()方法的源码很长,但主要逻辑主要分为三部分,第一部分:通过调用 select()从操作系统中轮询到网络 IO 事件;第二部分:处理 IO 事件;第三部分:处理 nioEventLoop 的任务队列中的普通任务和定时任务。我对 run()方法的源码进行了删减,精简后的源码如下。
在 run()方法中,会通过选择策略(selectStrategy )来计算 switch 语句中的条件值,在计算的时候,会先通过 hasTasks() 方法来判断 taskQueue 和 tailQueue 中是否有任务等待被执行,如果有任务,则将调用 selectNow()方法从操作系统中来轮询网络 IO 事件;如果没有任务,则将调用 select(timeout)方法来轮询网络 IO 事件。
为什么要这样做呢?因为 Netty 中为了保证任务被及时执行,selectNow()方法是个非阻塞方法,如果操作系统中没有已经准备好的网络 IO 事件,那么就会立即返回,有已经准备好的网络 IO 事件,那么就会将这些网络 IO 事件查询出来并立马返回。而 select(timeout)方法也是从操作系统中轮询网络 IO 事件,但是它是一个阻塞方法,当 netty 中有任务等待被执行时,使用阻塞方法,显然会造成任务被执行不及时的问题。
如果selectStrategy计算出来的值为-1,那么就会执行到下面这一行代码。
select(wakenUp.getAndSet(false))
这行代码首先会将 wakenUp 的值置为 false。wakenUp 字段表示的含义是是否需要唤醒 selector,在每次进行新的轮询时,都会将 wakenUp 设置为 false。然后调用 select()方法,从操作系统中轮询出来网络 IO 事件。
接着在 run()方法中会对 ioRatio 的值进行判断,ioRatio 的含义又是什么呢?在 Netty 中,NioEventLoop 每一次循环其实主要干两类事,一是处理网络 IO 事件,二是执行任务(包括普通任务和定时任务),但是处理这两类任务的所消耗的时间是不一样的。而且有些系统可能期望分配给处理网络 IO 事件的时间多一点,有些系统可能期望分配给处理任务的时间多一些,那么 netty 就需要提供一个变量来控制执行这两类事的所花的时间的占比,这个变量就是 ioRatio,翻译过来就是 IO 的时间占比。默认情况下,ioRatio 的值为 50,即处理网络 IO 的时间和处理任务的时间各占一半。所以默认情况下,会进入到 else 语句块中,在 else 语句块中,先进行了网络 IO 的处理(processSelectedKeys()),然后进行任务的处理(runAllTasks(timeoutNanos))。
接下来将对 run()方法中的三个主要部分进行详细分析。
3.轮询事件(select())
如果selectStrategy计算出来的值为-1,那么就会执行 select(oldWakenUp)方法。该方法的源码又是很长,为了方便阅读,我进行了删减,其中省略了解决 JDK 空轮询 bug 相关的代码,这一部分代码会在后面详细说明。精简后的源码如下。
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
// 当定时任务队列中没有任务时,select操作的截止时间为: 当前时间 + 1秒
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
// 1.定时任务截止时间快到了(<0.5ms),就跳出循环
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
// 如果还没有进行过select()操作,就执行一次selectNow()操作,selectNow()方法不会阻塞线程。
// 如果进行过select()操作,就直接跳出循环
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
// 轮询过程中,可能会有新的任务加入到任务队列中,就中断本次轮询,并将wakeUp设置为true
// 为什么呢?因为netty为了保证任务能够及时执行
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();