文章目录
前言
上一篇更多的是说结论,那结论是怎么来的呢?也是一步一步学习出来的,我在学习过程中,使用了Xmind,用来做思维导图还是比较好的,层次结构以及每个方法都可以很好的记录。本文重点分享一下下面这个类图以及每个类中的实现细节:
EventExecutorGroup和EventExecutor
这两个接口是上述类图中的基础,也是EventLoop的基础,特别是EventExecutorGroup,是netty自定义的第一个接口,先学习好它们,也就能学习好EventLoop了。
还是按照我的思路,先删减,后增补。
EventExecutorGroup
先看一下拆分的类图:
它本身继承了4个重要接口,以及它们各自的功能:
- Iterable:迭代器接口,返回一个迭代器。说明已经具备了Group的管理或者容器功能。
- Executor:具备了提交任务的能力。
- ExecutorService:具备了线程池的能力。
- ScheduledExecutorService:具备了执行调度任务的能力。
这4种能力不多说,都是JDK本身提供的,我们杰西莱看一下,它本身定义了哪些方法呢?如下图:
虽然有很多方法,但大多数都是上面接口的,重新写了一遍而已,我们对它新加的一些方法做一些分析。
- 它优化了JDK提供的关于中断的方法:标记过时(shutdown,shutdownNow),并且新加了几个方法
- boolean isShuttingDown();
- Future<?> shutdownGracefully();
- Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit);
- 增加了一个中断状态监听的方法
- Future<?> terminationFuture()。注意这个Future是netty的.
- 可以在这个future上面增加一些监听器(addListener),然后当中断的时候就会触发。
- 增加了next方法。返回的是由它管理的EventExecutor。这个组的味道已经出现了。这个也是后面实现轮询内部成员的一个关键方法。
这个接口的功能就是:线程池,管理一组EventExecutor,调度任务,中断监听。
我们单从Group的功能来看的话,这个接口下面就再也没有其它接口了,都是一些抽象类了,下面是一个比较简单的类图:
AbstractEventExecutorGroup
这个类没有任何的实现,仅仅只是把所有的提交任务的方法做了一下默认实现:就是调用一些next()方法轮询出来一个Executor然后去执行,比如:
@Override
public void execute(Runnable command) {
next().execute(command);
}
其它的所有方法都没有实现,也确实挺抽象的。
不过这个抽象类的作用还是非常大的,之前在分析组和成员关系的时候,组也具备了一些成员关系的功能,但是它的执行就是通过成员去完成的,而且这些功能本身也确实不需要组去关心的。而netty就是把这些功能放在了这个类里面,也放的特别合适,它没有实现任何有关于的组的管理功能,实际上就是交给子类去完成了,而子类在实现管理功能的时候,也就不需要关心这些功能了(提交任务),父类都已经实现了。
MultithreadEventExecutorGroup
听名字就可以听出来一些味道了:多线程事件执行器组。
3个关键词:多线程,事件执行器,组。
这个里面才是真正实现了组的管理功能,看一下它内部所有的属性和方法:
先看一下内部属性吧:
- EventExecutor[] children:固定大小的EventExecutor数组,构造器里面会根据大小全部初始化成员。
- EventExecutorChooser chooser:成员轮询器。虽然有两种实现,但其实都是按照(0,1,2,3,…,n-1)的顺序来的。只不过当大小是2的幂的时候,采用了一下位的和(&)运算,会稍微快一些。这个就是next方法的实现。
- terminationFuture:中断的一个Future,可以用来监听中断事件。
- readonlyChildren:用一个不可写的集合来做迭代器用。
- terminatedChildren:原子int,用来标记当前有多少个成员已经被中断了,当所有的都被中断的时候,就会触发中断事件。
这个类比较核心的就是它的构造器了,至于其它的关于中断的方法,基本上也都是循环中断所有的成员,不是特别复杂。当然还有一个创建成员的抽象方法,不过也是和构造器相呼应的。
关于它的构造器和一个抽象方法,我觉得也是比较有趣的点。
-
它的所有的构造器都是protected的,说明不是对外开放的,由子类去调用的。
-
最有趣的在于构造器的最后一个参数居然是:Object… args。在构造器里面,这种写法确实还是比较少见的。
-
这个类仅有一个抽象方法:
protected abstract EventExecutor newChild(Executor executor, Object... args) throws Exception
就是在初始化成员的时候需要调用的方法,而且它的最后一个参数也是 Object… args。其实构造器的对象数组的入参和这个入参是相呼应的,就是同一个。
-
这个构造器当时在定义的时候,开发人员估计费了不少的心思啊,哈哈。
-
那为什么要这样写呢?那肯定是为了复用,复用这些组的管理功能。它所管理的Executor到底需要哪些参数,以及如何来创建。它不关心,它也没法关心;所以留到了子类,同时它希望对于具体的子类而已,那个对象数组不能暴露给框架的使用人员,希望他们具体化,不能这么抽象的使用,所以构造器全部protected。
MultithreadEventExecutorGroup到这边就结束了,它更多的是完成了成员的初始化以及中断的相关内容。但是成员EventExecutor到底什么?且看下面的分析。
DefaultEventExecutorGroup
这个类非常简单,提供了3个构造器,实现了newChild方法,并且明确了成员就是DefaultEventExecutor。
当我学习到这边的时候,就基本非常明确了,它就是一个线程池,我第一个想到的就是和ThreadPoolExecutor进行比较,之前的文章以及比较过了。但是在比较之前,想了一下,我好像还并不知道任务是怎么执行的,也就是EventExecutor的具体实现了。
EventExecutor
这个接口才是真正的执行任务的接口:
它继承了EventExecutorGroup,说明它也拥有上面说的所有功能。
重点关注一下,它新定义的方法吧:
它基本上新定义了3类接口:
- parent():返回父节点。也反映了它被EventExecutorGroup管理的特性。
- inEventLoop():判断当前线程是不是当前EventExecutor所关联的事件循环的线程。非常重要的方法,好多地方都会用到。因为在后续的提交任务的时候,有可能是事件循环的线程(这个就是提交的任务在执行过程当中又提交了新的任务),有可能是其它线程。然后可能需要做一些线程安全方面的工作。
- 创建Promise,或者Future的方法。这个我没有用过,但是也了解了一些,简单说一下,可能不太对。这两种对象都代表的是异步执行的结果,前者相对于后者具备了写的功能,后者只可读。但是他们都具备结束然后事件通知的能力,那么谁来通知呢?就是当前EventExector所关联的线程去通知。也就是通过它创建的,都会用它的关联线程去执行通知任务。但是具体的应用场景还不太清楚。
以后就是它比Group多出来的方法,一方面体现了它是成员角色,另一方面它也可以做额外的事情。从下面开始,也就没有接口定义了,都是具体的类了。
然后再看一下DefaultEventExecutor的整体类图:
AbstractEventExecutor
这个类一方面实现了EventExecutor,另一方也继承了JDK提供的AbstractExecutorService。后面这个类更多的是提供提交一些任务的默认实现,也没有做具体的业务实现。
AbstractEventExecutor它本身也没有做太多事情,只是把接口和抽象类整合在了一起。稍微梳理一下吧:
-
parent:定义了父对象。
-
selfCollection:返回一个属于自己的迭代器。
-
promise,future的方法都做了实现,如下:
@Override public <V> Promise<V> newPromise() { return new DefaultPromise<V>(this); } @Override public <V> ProgressivePromise<V> newProgressivePromise() { return new DefaultProgressivePromise<V>(this); } @Override public <V> Future<V> newSucceededFuture(V result) { return new SucceededFuture<V>(this, result); } @Override public <V> Future<V> newFailedFuture(Throwable cause) { return new FailedFuture<V>(this, cause); }
-
关于定时任务的,都直接抛了异常,不支持。
-
提供了一个EventExecutorGroup的一个构造器。
没有做太多的事情,做一些默认实现。重点看一下它的子类。
AbstractScheduledEventExecutor
看这个名字基本也能猜出来,它实现了调度任务:
-
内部有一个优先级队列,最早执行的会放在队首,每次提交任务的时候会进行调整。它这个不是线程安全的队列。因此它在添加调度任务的时候,如果不是事件循环线程的话,会提交一个新的普通任务取提交任务,保证线程安全:
<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) { if (inEventLoop()) { scheduledTaskQueue().add(task); } else { execute(new Runnable() { @Override public void run() { scheduledTaskQueue().add(task); } }); } return task; }
-
所有被提交的调度任务都会被封装成ScheduledFutureTask,这个对象里面有几个关键点:
-
当这个类被内存加载的时候,会生成当前时间的一个纳秒时间戳。以后所有的时间计算都会以它作为开始时间的标准,比如获取当前纳米戳=获取当前纳秒时间戳的然后减去开始时间就是当前时间:
private static final long START_TIME = System.nanoTime(); static long nanoTime() { return System.nanoTime() - START_TIME; }
-
deadlineNanos:这个任务应该被执行的纳秒时间戳。都是通过比较这个属性值来判断当前任务是不是到时间去执行了。
-
periodNanos:调度任务的执行周期。为0的话,代表只执行一次。否则,就认为是周期不断执行的任务,间隔就是periodNanos。
-
关键在于它的run方法,解决了如何执行周期任务。当一个周期任务执行完了,它会把periodNanos加到deadlineNanos上面取,作为新的执行时间,然后重新加入队列。然后就可以再执行了:
@Override public void run() { assert executor().inEventLoop(); try { if (periodNanos == 0) { if (setUncancellableInternal()) { V result = task.call(); setSuccessInternal(result); } } else { // check if is done as it may was cancelled if (!isCancelled()) { task.call(); if (!executor().isShutdown()) { long p = periodNanos; if (p > 0) { deadlineNanos += p; } else { deadlineNanos = nanoTime() - p; } if (!isCancelled()) { // scheduledTaskQueue can never be null as we lazy init it before submit the task! Queue<ScheduledFutureTask<?>> scheduledTaskQueue = ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue; assert scheduledTaskQueue != null; scheduledTaskQueue.add(this); } } } } } catch (Throwable cause) { setFailureInternal(cause); } }
-
-
同时它也提供了一些取出到了执行时间任务的一些方法,供子类使用。
虽然它实现了提交定时任务,以及如何来完成周期任务的执行,但是要怎么触发并且执行它们呢?它没说。
SingleThreadEventExecutor
单线程事件执行器,意思以及很明确了,用一个线程去执行所有的事件。
这个类的属性和方法有点多,生成的图不好看,IDEA也不能删除其中几个。那就没有图了,调几个重要的:
private final Queue<Runnable> taskQueue;
private volatile Thread thread;
private final boolean addTaskWakesUp;
private final int maxPendingTasks;
简单说一下这几个属性吧:
- taskQueue:任务队列,所有提交的任务会执行扔到这个队列里面。注意它的定义并不是阻塞队列。但是它创建的时候,提供的默认实现就是阻塞队列,并且它提供的一个内部方法takeTask(),取任务的时候还强制要求必须是阻塞队列,否则就抛异常。刚开始还挺郁闷的,想不明白。最后发现创建队列的方法被NIOEventLoop给重写了,它那边提供的是一个无锁高性能队列MPSC(多生茶这,单消费者,这就是它的消费模型)队列,这个并不是阻塞队列。这或许解释得通?
- thread:事件循环的线程。会在线程启动以后赋值这个变量,它是用Executor启动的,而不是直接一个线程。
- addTaskWakesUp:如果为true,意味着:当且仅当执行addTask(Runnable)方法的时候,会唤醒执行器的线程。我对这个变量没有搞太明白,它这边提供的构造器默认是true,而EventLoop那边变成了false,而且我很明确它为false的时候提交一个新任务进来才可以唤醒正在阻塞轮询的线程。它说明中的addTask方法,也是仅仅是提交任务的时候会去触发:
重点可以看一下后面两行,它必须为true,才会去执行wakeUp方法。所以对于它的描述,实现是看不明白。不解释了,有明白的可以分享一下。@Override public void execute(Runnable task) { if (task == null) { throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
- maxPendingTasks:队列的最大容量。
从功能上来讲,这个类主要做了这么几件事情:
- 任务队的创建。
- 循环线程的启动,当提交第一个任务的时候就会启动线程:
private void startThread() {
if (state == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
try {
doStartThread();
} catch (Throwable cause) {
STATE_UPDATER.set(this, ST_NOT_STARTED);
PlatformDependent.throwException(cause);
}
}
}
}
private void doStartThread() {
assert thread == null;
executor.execute(new Runnable() {
@Override
public void run() {
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
boolean success = false;
updateLastExecutionTime();
try {
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
}
//其它的省略一下
}
});
}
启动以后,它重点就做了一个事情,执行run方法。这是个抽象方法。也就是真正的循环体这边也没有实现。
- 任务的提交。实现了execute方法,这也是非常关键的方法。
- 提供了一些从队列里面取任务的方法,供子类用,毕竟它定义队列成private了,比如:
- Runnable pollTask()
- Runnable pollTaskFrom(Queue taskQueue)
- Runnable takeTask()
- boolean fetchFromScheduledTaskQueue()
- Runnable peekTask()
- 还提供了一些执行任务的方法,比如:
- boolean runAllTasks()
- runAllTasksFrom(Queue taskQueue)
- runAllTasks(long timeoutNanos)
它的职责就这些,该做的都做了,但是我要怎么循环取任务呀,好像也没说。这就剩下最关键的一个方法run了。它也是EventLoop最大的区别了。
DefaultEventExecutor
默认的事件执行器。
它实现了run方法,无限循环:从队列里面取出每一个任务,然后去执行:
@Override
protected void run() {
for (;;) {
Runnable task = takeTask();
if (task != null) {
task.run();
updateLastExecutionTime();
}
if (confirmShutdown()) {
break;
}
}
}
然后就完了,这个类其实也挺简单的。
到此,成员Executor就结束了。不知道了有没有讲清楚,可能也有一些细节都忽略掉了,不过整体它的运行机制应该都讲到了。
话说写到这边的话,它的篇幅已经超过了第一篇了,没有想到写了这么多。主角EventLoop都还没有上场呢。。。还是那句话,这个整明白了,EventLoop也就比较容易了,事实也是如此。
EventExecutorGroup与EventExecutor
上面一直在说这两个,说完了,我再放一张图吧,不解释了,上篇文章里面有:
我是按照类图的递进关系来学习和讲解的,每个类的职责都会说到,虽然我内心是比较明白的,但是总感觉好像缺了一张有关于每个类的职责的循序渐进图,可以很明显的表现出它们之间的关系,我也没有画。
不过话说回来,对于我们学习者而言,结论重要还是它的整个学习过程重要呢?我的学习初衷就是想了解它的运行机制,仅此而已。而关于类的职责划分,或许这是当时框架的开发者去思考的,但确实我们学习的人也能站在它们的角度去思考,更方面我们的理解,但是,一定要把握好度,差不多就行了。因此,上面那个图,有了更好,没有也行。
接下来看一下重头戏吧。
EventLoopGroup和EventLoop
从我的学习过程看,当学习完上面两个,这边的内容已经是特别少了,主要增加了通道(Channle)和多路复用器(Selector)的内容。
放一张NIOEventLoopGroup和NIOEventLoop的类图吧:
EventLoopGroup
直接看一下EventLoopGroup到NIOEventLoopGroup的类图吧:
EventLoopGroup,它继承了EventExecutorGroup,所以我们上面讲的功能它都具备。
看一下它的接口定义:
4个方法:
- next():覆盖了父类的方法,把返回值换成了EventLoop。
- register(Channel channel):注册通道。
- register(ChannelPromise promise):注册通道。
- register(Channel channel, ChannelPromise promise):废弃的注册通道的方法。
注意它的第一个注册的实现还是调用了第二个方法,因此说白了,它就增加了一个方法:注册通道。没了。
关于这个方法多说两句哈,使用NIO编程的时候,我们需要使用创建一个SocketChannel,然后再把它注册在多路复用器上面。而netty也是通过这个方法把通道绑定在了EventLoopGroup所管理的其中一个EventLoop中关联的多路复用器上面。和java的nio结合了起来。但其实EventLoop接口本身并没有定义有关多路复用器相关的操作;因此在实现这个方法的时候,AbstractNioChannel所依赖的就是很明确的NIOEventLoop了,属性定义虽然是接口,但是用的时候进行强行类型转换了。这个其实还是挺郁闷的。
EventLoop上面为什么不定义和多路复用器相关的方法。这个可能目前能想来一点点,因为能看见的它的实现有好几种(还没有研究它们有什么用),但只有NIOEventLoop使用到了多路复用器。
突然提到了EventLoop,剧透一下,它继承了EventLoopGroup,然而一个新的方法都没有定义。
Channel中的属性为什么没有直接定义成NIOEventLoop呢?在子类AbstractNioChannel中直接强转了,这边代码看得少,再看看或许会有答案的。
不过就算想不明白,也不用去纠结这个。因为完全不影响整体的一个分析。
MultithreadEventLoopGroup
这个类做的事情更少,就做了一个事情:把MultithreadEventExecutorGroup(上面讲过了)和EventLoopGroup结合在了一起。
那些注册通道之类的也都是调用next()方法让EventLoop去做了。
额外还有个事情,它把这个里面的线程的优先级设为了10(最高),MultithreadEventExecutorGroup这个里面只有5。也是为了优先处理IO请求吧。
NioEventLoopGroup
实现了newChild方法,创建成员的时候,返回的就是NioEventLoop。
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
增加了几个方法:
- setIoRatio:设置所有的成员的IO比例
- rebuildSelectors:重置所有成员的多路复用器
这两个方法没有定义在接口上面,可能是因为最后的实现类里面才有了这两个方法。
然后就没了~~~突然觉得好简单啊,确实就这么简单。
EventLoop
来个NIOEventLoop的简单类图瞅一下:
它继承了EventLoopGroup和EventExecutor(上面讲过了),但是如我上面所说的,它啥都没有定义,看看:
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
@Override
EventLoopGroup parent();
}
就没了,所以也没有可讲的了。
SingleThreadEventLoop
它继承了SingleThreadEventExecutor(上面讲过了),实现了EventLoop,也就是把两者结合在一起了。
它实现了注册通道的方法:
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
@Override
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}
看这个代码,真的是什么都看不出来。
写过NIO代码的应该就会理解,具体的注册逻辑交给了通道本身,这个和jdk的实现策略是一致的。它一行代码内部肯定会执行NIO的原生代码的。
增加了一个看着不知道有什么用的功能,增加了tailQueue,也可以往里面加一些任务,在每次事件轮询之后执行。不知道有啥用。
好像也没有其它重点内容了。越来越简单了。
NIOEventLoop
这个类是真的一点都不简单,毕竟加入了多路复用器,不过我不详细讲了,因为网上讲得非常多,也非常细,我还是强调一些关键点吧。
(我直接抄的第一篇里面的)
- EventLoop里面关键属性有两个,多路复用器Selector和任务队列。可以把通道(Channel)注册在多路复用器上面,可以不断轮询其中的事件然后执行。任务队列存储提交的task。
- EventLoop处理的事件(叫任务也行,事件更加贴切吧)整体上有两种:
- IO事件。当一个EventLoop所关联的多路复用器上面注册的通道发生“连接、接收(Acceptor)、读、写”事件的时候,就相当于触发了IO事件。一般也就两种场景:作为server端的时候,监听一个端口,别人来访问你的端口,就会先触发接收事件,然后读取,写入事件。作为client端的时候,要和目标连接,连接成功以后就会触发连接事件,然后写入,读取事件。(场景简化了一下)
- 非IO事件。这边又分为两种:
- 普通任务。使用execute提交的任务,直接执行的。
- 调度任务。使用schedule提交的任务,一般需要延迟或者周期性执行的。
- EventLoop在执行的时候,也是无线循环,循环体内主要有3件事:阻塞轮询、执行IO事件和执行非IO事件。
- 若当前没有任务非IO事件(普通任务)需要执行,且在0.5s内没有需要执行的调度任务的时候,先会进入一个无限循环,里面会调用多路复用器的select(long)方法进行阻塞超时轮询,阻塞超时默认是1s或者有定时任务的话,就取定时任务应该执行的时间与当前时间的间隔为超时时间(意思就是,我超时结束的时候,最早的定时任务刚好可以执行了)。
- 多路复用器的阻塞超时轮询,并不会一直等到超时,有多种方式可以唤醒它:
- 多路复用器已经准备好了至少一个事件;基本上就是有IO事件的话,就直接返回了,不会阻塞。
- 使用wakeup方法。当其它线程调用的时候,会立刻唤醒正在阻塞轮询多路复用器的线程。而EventLoop也是利用了这一点,当有新的任务提交进来,并且当前情况满足4个条件的话,就会执行wakeUp。条件很好满足。而且其中某些条件就是在判断是不是在做阻塞轮询,如果是的话,才会去唤醒。
- 当正在阻塞轮询的时候,有新的非IO任务进来的话,就会立刻唤醒。和上一点是一回事,换了一种说法。
- 这边也有一个骚操作,它在执行一些中断操作的时候,会提交一个空任务来唤醒。
- 超时时间到。
- 当前线程被中断。后面这两种没有什么可说的。
- 它的这种唤醒机制,保证了不会影响到任何事件。但是仔细想想,这也是应该的,毕竟是它实在没有事情做的时候,才回去阻塞轮询,因为对于NIO而已,根本不需要进行阻塞,你去忙你的,忙完了回来叫我,我都给你准备好了,你忙你的,我做我的,相互不影响(你=eventLoop,我=多路复用器)。正因为如此,它的代码实现上面,对于跳出无限阻塞轮询(阻塞轮询外层有个无限循环)的条件也是非常开放(不知道怎么描述了),很容易就跳出了,可以看看代码。
- 阻塞轮询完了或者根本不需要阻塞轮询的(有非IO事件),就要处理事件了。它这边有个IO比例,默认是50,就是IO:非IO=50:50,比如处理IO的时间是100ms,那么处理非IO的时间最大也得是100ms,但是它并没有强行去限制,也确实不好做。它仅仅只是在每执行64个非IO事件以后去判断一下这个时间,超了的话,就停下来。64,也不知道是怎么定义的,说实话我觉得挺多的,太小的话,是不是就会影响到非IO任务的执行了呢?还有这个IO比例,当=100的时候,就完全忽略了时间比,每轮询一次,就会把剩余的所有非IO全部执行完。既然都是IO比例了,这种情况就不应该是只执行IO吗?只执行IO肯定不对,但是这个实现和对应的情况实在是不搭呀,理解不了。或许是因为有些事情我还没有理解透彻。
- 执行IO的时候,就是把所有轮询到的事件,挨个去执行。这块就是我开篇提到的第四个核心,不过我还没有细看(主要是挺复杂的,不花点事情是搞不明白的),就不说了。反正是一个一个执行IO事件,而且肯定是用当前线程去执行,但是肯定不会花太多时间去处理完的,到最后一定会交给另外一个EventLoopGroup,这也是标准的Reactor模型。netty服务端启动的时候,需要提供两个EventLoopGroup,也是这个作用吧,我猜的。
- 执行非IO的时候,先把调度队列中所有到期的取出来放进任务队列中,然后挨个去执行。一个是全部执行完,一个有时间限制。执行完了以后,会执行tailTasks队列里面的任务,这个设计不知道用来干嘛的,意思就是每一次轮询结束,就去执行一下。感觉没有什么用呀。
- 结束以后,下一波轮询又开始了。
- 它再内部阻塞轮询多路复用器的时候,也修复了JDK的epoll bug。
- bug描述:它会导致Selector空轮询,IO线程CPU 100%,严重影响系统的安全性和可靠性。
- 修复思路:
- 根据该BUG的特征,首先侦测该BUG是否发生:正常情况下,开始时间+阻塞轮询时间<=当前时间;这个是正常的;但是如果反过来的话,就不正常了。实际上阻塞的时间比预期的时间会小,不符合javadoc的描述,就认为做了一次空轮询。当空轮询次数超过默认值512次时,就去重新构建多路复用器。
- 将问题Selector上注册的Channel转移到新建的Selector上
- 老的问题Selector关闭,使用新建的Selector替换
- 它内部还有一个比较重要的原子性的布尔值:wakeUp。它是用来确定是否需要唤醒正在使用阻塞轮询多路复用器的线程(就是EventLoop的线程)。
- true:代表应该被唤醒或者已经被唤醒了(它有的地方会判断为ture的时候,会立即唤醒,之后也不会修改它的状态)
- false:代表应该去阻塞轮询了或者正在阻塞轮询。
- 修改的它的位置有3个:
- 开始打算轮询的时候,会置为false(select(boolean)方法)。代表我马上要阻塞轮询了。
- 在无限轮询的循环体内,每次都会判断:有新任务并且是false的时候,会置为true,然后跳出。这个应该是来解决,当添加任务不满足4个条件的时候,就不会触发唤醒;这个是每次阻塞轮询前判断,也就是有的任务添加进来,虽然不会立即唤醒阻塞轮询线程,但是当阻塞结束的时候,它一定就会跳出循环。结束,有新任务进来了。
- 添加任务的时候,如果是false,会置为true。wakeup(boolean inEventLoop)。添加任务需要触发唤醒,需要满足4个条件。
EventLoopGroup和EventLoop
在放一张结论图:
到这边就彻底结束了。
结语
不知道有没有分享清楚我的学习过程以及EventLoop的运行机制?
不过我觉得还缺了一篇,结合IO模型分析EventLoop的使用场景,这样会更好的体会到EventLoop的强大以及加深对它的了解。我觉得我应该会写,但不会太快。
学习初衷很重要,在学习过程当中可以把握住重点,忽略掉一些干扰因素,提高自己的学习效率,但是也要记录下来哪些应该学习而没有学的。
学习方式也很重要,要知道自己怎么去学习,学习之前获取就要想明白一些。