文章目录






一、前言

netty最核心的就是reactor线程,对应项目中使用广泛的NioEventLoop,本文核心:Netty中Reactor线程启动+Reactor线程三步骤执行。

二、Reactor线程的启动

2.1 类图

NioEventLoop 父类 SingleThreadEventExecutor 的execute方法

SingleThreadEventExecutor 单线程事件执行器,它是NioEventLoop的父类,

MultithreadEventExecutorGroup 多线程事件执行器组,它是NioEventLoopGroup的父类

如下:

乘风破浪,Netty中的Reactor全解析_任务队列

2.2 源码解析:NioEventLoop父类 SingleThreadEventExecutor类中的execute方法

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

这里我们看NioEventLoop类的父类的SingleThreadEventExecutor

NioEventLoop父类 SingleThreadEventExecutor类中的execute方法,如下

public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}

boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startExecution();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}

if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}

NioEventLoop父类 SingleThreadEventExecutor类中的execute方法,去掉前面和后面的,取出中间重要的讲:

@Override
public void execute(Runnable task) {
...
boolean inEventLoop = inEventLoop(); // 如果返回为false,不是线程不相等,因为单线程不会再有其他线程了,因为是线程还没有启动,启动线程即可
if (inEventLoop) {
addTask(task); // 添加任务
} else {
startThread(); // 下面看startThread()的详细方法,新版 startExecution(); 都是一样的,
addTask(task); // 先启动线程,再添加任务
...
}
...
}
public boolean inEventLoop() {   //  当前线程在事件循环里面,返回为true,当前线程不在事件循环里面,返回为false
return this.inEventLoop(Thread.currentThread());
}
// 调用,就是看传入的实参thread引用是否等于单线程事件执行器里面的线程thread (即this.thread)
// 单线程执行器里面的只有一个线程,所以是this.thread,
// 如果返回为false,不是线程不相等,因为单线程不会再有其他线程了,因为是线程还没有启动,启动线程即可
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}

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

private void startThread() {  // 这是startThread()的详细方法
if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { // cas保证线程安全,在线程安全的情况下执行doStartThread()方法,没有其他业务逻辑
doStartThread(); // 下面看doStartThread()的详细方法
}
}
}

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

private void doStartThread() {    // 这是doStartThread()的详细方法
...
executor.execute(new Runnable() { // 下面看execute方法的实体
@Override
public void run() {
thread = Thread.currentThread(); // 当前线程赋值给thread变量
...
SingleThreadEventExecutor.this.run();
...
}
}
}

金手指:Reactor线程启动

netty的reactor线程在添加一个任务的时候被创建,该线程实体为 FastThreadLocalThread(就是Netty的reactor线程),最后线程执行主体为NioEventLoop的run方法。

创建Reactor线程的时候,方法调用栈:

SingleThreadEventExecutor类中的execute()方法 --> SingleThreadEventExecutor类中的startThread()方法 --> SingleThreadEventExecutor类中的doStartThread()方法

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

接上面,executor变量是ThreadPerTaskExecutor类对象,要知道,这个executor在每次执行execute 方法的时候都会通过DefaultThreadFactory创建一个FastThreadLocalThread线程(就是netty中的Reactor线程),而这个在执行execute()方法的时候被创建出来的FastThreadLocalThread线程就是netty中的reactor线程实体。

2.3 源码解析:newThread()方法,ThreadPerTaskExecutor类中使用工厂模式新建线程并启动

public final class ThreadPerTaskExecutor implements Executor {
private final ThreadFactory threadFactory;

public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");
}

@Override
public void execute(Runnable command) { // 这是ThreadPerTaskExecutor类中的execute方法的实体
threadFactory.newThread(command).start();
}
}

问题:源码解析threadFactory.newThread(),使用工厂模式来new一个对象,为什么是 ThreadPerTaskExecutor 和 DefaultThreadFactory的组合来new一个FastThreadLocalThread?

回答:标准的netty程序会调用到NioEventLoopGroup的父类MultithreadEventExecutorGroup

MultithreadEventExecutorGroup类构造函数:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
}

将初始化的好的ThreadPerTaskExecutor对象,通过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]);
}

小结:newChild()返回一个NioEventLoop类对象,这个NioEventLoop类对象里面放着ThreadPerTaskExecutor对象。

关于reactor线程的创建和启动就先讲这么多,我们总结一下:netty的reactor线程在添加一个任务的时候被创建,该线程实体为 FastThreadLocalThread,最后线程执行主体为NioEventLoop的run方法。

三、reactor 线程的执行(三步骤)

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

@Override
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));

if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}
processSelectedKeys();
runAllTasks(...);
}
} catch (Throwable t) {
handleLoopException(t);
}
...
}

我们抽取出主干,reactor线程做的事情其实很简单,一共三个步骤:首先轮询注册到reactor线程,对用的selector上的所有的channel的IO事件;然后处理产生网络IO事件的channel;最后处理任务队列。reactor线程大概做的事情分为对三个步骤不断循环:

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

select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}

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

processSelectedKeys();

3.处理任务队列

runAllTasks(...);

接下来,对本文第四部分、第五部分、第六部分,对每个步骤详细说明。

四、Reactor线程执行第一步:轮询IO事件

4.1 轮询IO事件:宏观代码Demo

select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}

可以改为

boolean oldWakenUp = wakenUp.getAndSet(false);  // 
select(oldWakenUp);
if (wakenUp.get()) {
selector.wakeup();
}

AtomicBoolean类中的getAndSet()方法

public final boolean getAndSet(boolean newValue) {  // false
boolean prev; //局部变量
do {
prev = get();
} while (!compareAndSet(prev, newValue)); // 循环 + cas保证线程安全条件下,设置prev
return prev;
}

修改为

public final boolean getAndSet(boolean newValue) {  // false
boolean prev; //局部变量
for (;;){
prev=get(); // 这才是实际逻辑
// cas仅仅返回true|false作为线程安全的判断依据,不参与实际逻辑,和synchronized一样
if (compareAndSet(prev, newValue)){
break;
}
}
return prev;
}
public final boolean get() {   // 这才是实际逻辑
return value != 0;
}

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

金手指:Reactor线程执行第一步:轮询找到IO事件

第一步,Netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始;在​​select(wakenUp.getAndSet(false))​​中轮询找到IO事件,就是一个for死循环,直到找到IO事件才使用break跳出,找到IO事件跳出包括:

(1)定时任务截止事时间快到了,break跳出for循环,中断本次轮询;

(2)轮询过程中发现有任务加入,break跳出for循环,中断本次轮询;

(3)上面两个步骤可以找到IO事件交个第二第三步骤处理,所以第一步骤先select阻塞下来,阻塞第一个定时任务的截止时间,select()阻塞完成后,后面还有五种跳出轮询的方式

(4)Netty如何处理Java NIO的空转

第二步,跳出select方法的for循环后,唤醒

if (wakenUp.get()) {
selector.wakeup();
}

4.2 轮询IO事件select(wakenUp.getAndSet(false)):源码解析

4.2.1 NioEventLoop类select()方法:定时任务截止事时间快到了,break跳出for循环,中断本次轮询

int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
....
}
protected long delayNanos(long currentTimeNanos) {
ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
if (scheduledTask == null) {
return SCHEDULE_PURGE_INTERVAL;
}
return scheduledTask.delayNanos(currentTimeNanos);
}

金手指:NioEventLoop类select()方法:定时任务截止事时间快到了,中断本次轮询

回答:

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

(2)这里说明一点,netty里面定时任务队列是按照延迟时间从小到大进行排序,

delayNanos(currentTimeNanos)方法即取出第一个定时任务的延迟时间,排序方式后面定时任务队列(即优先级队列中有讲到)

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

4.2.2 轮询过程中发现有任务加入,break跳出for循环,中断本次轮询

for (;;) {
// 1.定时任务截至事时间快到了,中断本次轮询
...
// 2.轮询过程中发现有任务加入,中断本次轮询
if (hasTasks() && wakenUp.compareAndSet(false, true)) { // cas仅仅提供一个true|false作为线程操作安全的判断条件,这里表示有新线程加入且线程安全条件下,就执行一次非阻塞select操作(就是selector.selectNow();),跳出循环
selector.selectNow();
selectCnt = 1;
break;
}
....
}

金手指2:轮询过程中发现有任务加入,中断本次轮询

(1)netty为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,表示轮询过程中有任务加入,就执行一次非阻塞select操作(就是​​selector.selectNow();​​),跳出循环;

(2)注意:cas仅仅提供一个true|false作为线程操作安全的判断条件,这里表示有新线程加入且线程安全条件下,就执行一次非阻塞select操作(就是​​selector.selectNow();​​),跳出循环。

4.2.3 阻塞式select操作,五个条件中只要满足任何一个条件,就会break跳出for循环,中断轮询

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


金手指3:阻塞式select操作,五个条件中只要满足任何一个条件,就会break跳出for循环,中断轮询

3.1 进入到第三步的两个条件 执行到这一步,说明netty任务队列里面队列为空(如果任务队列不为空,如果轮询过程总有新任务加入,在第二步就会跳出循环),并且所有定时任务延迟时间还未到(大于0.5ms)(如果有一个定时任务的剩余时间小于0.5ms,在第一步就会跳出循环),所以已经轮询找到IO事件交给第二步骤和第三步骤处理了,所以第一步骤轮询找到IO事件这里先阻塞下来,阻塞到第一个定时任务的截止时间,先歇一歇

3.2 第三步开始,

3.2.1 select阻塞(selector.select(timeoutMillis); 这行代码是阻塞,在学习Reactor三种模式的时候就知道了):首先在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间(为什么timeoutMillis是第一个任务的截止时间,因为到这里来的第一个任务的截止时间就阻塞了,后面的无法到这里来)

3.2.2 select阻塞完成后,还有五个条件可以break循环,中断轮询:阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有(只要满足任何一个条件,就会break;跳出for循环,中断轮询)


  1. 轮询到IO事件 (selectedKeys != 0)
  2. oldWakenUp 参数为true (这就是传来进来的实参的唯一作用了,提供一种跳出循环的条件
  3. 任务队列里面有任务(hasTasks)
  4. 第一个定时任务即将要被执行 (hasScheduledTasks())
  5. 用户主动唤醒(wakenUp.get())


问题:如果第一个定时任务的延迟非常长,比如一个小时,那么有没有可能线程一直阻塞在select操作?
回答
:当然有可能!But,只要在这段时间内,有新任务加入,该阻塞就会被释放。具体解释如下:

// 外部线程调用execute方法添加任务
@Override
public void execute(Runnable task) {
...
wakeup(inEventLoop); // inEventLoop为false
...
}
// 调用wakeup方法唤醒selector阻塞
protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}

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

4.2.4 重点问题:Netty如何解决jdk的nio bug

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

long currentTimeNanos = System.nanoTime();
for (;;) {
// 1.定时任务截止事时间快到了,中断本次轮询
...
// 2.轮询过程中发现有任务加入,中断本次轮询
...
// 3.阻塞式select操作
selector.select(timeoutMillis); // 这行代码是阻塞,在学习Reactor三种模式的时候就知道了
// 4.解决jdk的nio bug
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {

rebuildSelector();
selector = this.selector;
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
...
}

金手指4:Netty如何解决jdk的nio bug

总述:netty没有从根本上解决jdk的bug,而是通过一种方式来巧妙地避开了这个bug

分述1:netty 会在每次进行 selector.select(timeoutMillis)之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒(这里将time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或许更好理解一些),

如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志,如果持续的时间小于timeoutMillis,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector

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

int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
selectorAutoRebuildThreshold = 0;
}

SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

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

public void rebuildSelector() {
final Selector oldSelector = selector; // selector赋值给oldSelector
final Selector newSelector;
newSelector = openSelector(); // 首先,通过openSelector()方法创建一个newSelector

int nChannels = 0;
try {
for (;;) { // 然后执行一个死循环,
for (SelectionKey key: oldSelector.keys()) { // 1、拿到有效的key
Object a = key.attachment(); // 得到key对应的attachment
if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
continue;
}
// 只要执行过程中出现过一次并发修改selectionKeys异常,就重新开始转移
int interestOps = key.interestOps();
key.cancel(); // 2、取消该key在旧的selector上的事件注册
SelectionKey newKey = key.channel().register(newSelector, interestOps, a); // 3、将该key对应的channel注册到新的selector上
if (a instanceof AbstractNioChannel) {
((AbstractNioChannel) a).selectionKey = newKey; // 4、重新绑定channel和newkey的关系
}
nChannels ++;
}
break;
}
} catch (ConcurrentModificationException e) {
// Probably due to concurrent modification of the key set.
continue;
}
selector = newSelector; // 后面所有的轮询都是在新的selector进行
oldSelector.close(); // 转移完成之后,就可以将原有的selector废弃
}

分述2:如果持续的时间小于timeoutMillis,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector,介绍一下如何重建selector?

总述 2.1 new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上;

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

具体的转移步骤为


  1. 拿到有效的key(for循环中得到的key)
  2. 取消该key在旧的selector上的事件注册
  3. 将该key对应的channel注册到新的selector上,得到newkey
  4. 重新绑定channel和newkey的关系

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

4.3 小结

问题:reactor线程select步骤做的事情?

回答:for循环来实现不断地轮询是否有IO事件发生,并且在轮询的过程中不断检查是否有新任务加入(定时任务和普通任务均可 第二步骤),保证了netty的任务队列中的任务得到有效执行,轮询过程顺带用一个计数器避开了了jdk空轮询的bug(第四步骤),过程清晰明了。

五、Reactor线程执行第二步:处理轮询到的事件

Netty reactor线程的第一步是轮询出注册在selector上面的IO事件(select),那么接下来就要处理这些IO事件(process selected keys)。

5.1 processSelectedKeys():处理轮询到(就是selected)的事件

我们进入到reactor线程的 run 方法,找到处理IO事件的代码,如下

processSelectedKeys();

跟进去

private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

我们发现处理IO事件,netty有两种选择,从名字上看,一种是处理优化过的selectedKeys,一种是正常的处理。

5.2 processSelectedKeysOptimized(selectedKeys.flip());总述

5.2.1 processSelectedKeysOptimized(selectedKeys.flip());的优化:SelectedSelectionKeySet 类

我们对优化过的selectedKeys的处理稍微展开一下,看看netty是如何优化的,我们查看 selectedKeys 被引用过的地方,有如下代码:

private SelectedSelectionKeySet selectedKeys;

private Selector NioEventLoop.openSelector() {
//...
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
// selectorImplClass -> sun.nio.ch.SelectorImpl
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
//...
selectedKeys = selectedKeySet;
}

首先,selectedKeys是一个 SelectedSelectionKeySet 类对象,在NioEventLoop 的 openSelector 方法中创建,之后就通过反射将selectedKeys与 sun.nio.ch.SelectorImpl 中的两个field绑定

sun.nio.ch.SelectorImpl 中我们可以看到,这两个field其实是两个HashSet

// Public views of the key sets
private Set<SelectionKey> publicKeys; // Immutable
private Set<SelectionKey> publicSelectedKeys; // Removal allowed, but not addition
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = new HashSet<SelectionKey>();
selectedKeys = new HashSet<SelectionKey>();
if (Util.atBugLevel("1.4")) {
publicKeys = keys;
publicSelectedKeys = selectedKeys;
} else {
publicKeys = Collections.unmodifiableSet(keys);
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
}

selector在调用select()族方法的时候,如果有IO事件发生,就会往里面的两个field中塞相应的selectionKey(具体怎么塞有待研究),即相当于往一个hashSet中add元素,既然netty通过反射将jdk中的两个field替换掉,那我们就应该意识到是不是netty自定义的SelectedSelectionKeySet在add方法做了某些优化呢?

带着这个疑问,我们进入到 SelectedSelectionKeySet 类中探个究竟

final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {

private SelectionKey[] keysA;
private int keysASize;
private SelectionKey[] keysB;
private int keysBSize;
private boolean isA = true;

SelectedSelectionKeySet() {
keysA = new SelectionKey[1024];
keysB = keysA.clone(); // keysA克隆一个到keysB
}

@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}

if (isA) {
int size = keysASize;
keysA[size ++] = o;
keysASize = size;
if (size == keysA.length) {
doubleCapacityA();
}
} else {
int size = keysBSize;
keysB[size ++] = o;
keysBSize = size;
if (size == keysB.length) {
doubleCapacityB();
}
}

return true;
}

private void doubleCapacityA() {
SelectionKey[] newKeysA = new SelectionKey[keysA.length << 1];
System.arraycopy(keysA, 0, newKeysA, 0, keysASize);
keysA = newKeysA;
}

private void doubleCapacityB() {
SelectionKey[] newKeysB = new SelectionKey[keysB.length << 1];
System.arraycopy(keysB, 0, newKeysB, 0, keysBSize);
keysB = newKeysB; // 扩容一倍
}

SelectionKey[] flip() {
if (isA) {
isA = false; // 翻转 唯一设置isA的地方
keysA[keysASize] = null;
keysBSize = 0;
return keysA; // 这里返回一个SelectionKey数组元素,因为是isA==true进来的,这里返回keysA
} else {
isA = true; // 翻转 唯一设置isA的地方
keysB[keysBSize] = null;
keysASize = 0;
return keysB; // 这里返回一个SelectionKey数组元素,因为是isA==false进来的,这里返回keysB
}
}

@Override
public int size() { // 这里返回大小
if (isA) {
return keysASize;
} else {
return keysBSize;
}
}

@Override
public boolean remove(Object o) { // 默认无操作
return false;
}

@Override
public boolean contains(Object o) { // 默认无操作
return false;
}

@Override
public Iterator<SelectionKey> iterator() { // 默认误操作,抛出一个异常
throw new UnsupportedOperationException();
}
}

关于netty对SelectionKeySet的优化我们暂时就跟这么多,下面我们继续跟netty对IO事件的处理,转到processSelectedKeysOptimized

5.2.2 processSelectedKeysOptimized(selectedKeys.flip());源码解析

private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
// 1.取出IO事件以及对应的channel
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null;
final Object a = k.attachment();
// 2.处理该channel
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
// 3.判断是否该再来次轮询
if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}

我们可以将该过程分为以下三个步骤

5.2.2.1 取出IO事件以及对应的netty channel类


相关问题:为什么拿到当前SelectionKey之后,将selectedKeys[i]置为null?
回答
:Java四种引用类型,不设置为null就是强引用,GC root可达的,很容易造成gc不掉,内存泄漏就发生了。

想象一下这种场景,假设一个NioEventLoop平均每次轮询出N个IO事件,高峰期轮询出3N个事件,那么selectedKeys的物理长度要大于等于3N,如果每次处理这些key,不置selectedKeys[i]为空,那么高峰期一过,这些保存在数组尾部的selectedKeys[i]对应的SelectionKey将一直无法被回收,SelectionKey对应的对象可能不大,但是要知道,它可是有attachment的,这里的attachment具体是什么下面会讲到,但是有一点我们必须清楚,attachment可能很大,这样一来,这些元素是GC
root可达的,很容易造成gc不掉,内存泄漏就发生了


5.2.2.2 处理该channel(attachment两种类型:AbstractNioChannel)

5.2.2.2.1 attachment第一种类型:AbstractNioChannel


1.1 注册到selctor上的attachment的第一种类型,就是 AbstractNioChannel
证据:满足条件 if (a instanceof AbstractNioChannel)
相关问题:为什么满足条件 if (a instanceof AbstractNioChannel)
回答:netty的轮询注册机制其实是将AbstractNioChannel内部的jdk类SelectableChannel对象注册到jdk类Selctor对象上去,并且将AbstractNioChannel作为SelectableChannel对象的一个attachment附属上,这样再jdk轮询出selector某条SelectableChannel有IO事件发生时,就可以直接取出AbstractNioChannel进行后续操作
1.2 详细的 processSelectedKey(SelectionKey k, AbstractNioChannel ch) 过程
(1)对于boss NioEventLoop来说,轮询到的是基本上就是连接事件,后续的事情就通过他的pipeline将连接扔给一个worker NioEventLoop处理
(2)对于worker NioEventLoop来说,轮询到的基本上都是io读写事件,后续的事情就是通过他的pipeline将读取到的字节流传递给每个channelHandler来处理


拿到对应的attachment之后,netty做了如下判断

if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
}

相关问题:为啥会有这么一条判断,凭什么说attachment可能会是 AbstractNioChannel对象?

我们的思路应该是找到底层selector, 然后在selector调用register方法的时候,看一下注册到selector上的对象到底是什么鬼,我们使用intellij的全局搜索引用功能,最终在 AbstractNioChannel中搜索到如下方法

protected void doRegister() throws Exception {
// ...
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
// ...
}
protected SelectableChannel javaChannel() {
return ch;
}
//*
//* @param sel
//* The selector with which this channel is to be registered
//*
//* @param ops
//* The interest set for the resulting key
//*
//* @param att
//* The attachment for the resulting key; may be <tt>null</tt>
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;

javaChannel() 返回netty类AbstractChannel对应的jdk底层channel对象

我们查看到SelectableChannel方法,结合netty的 doRegister() 方法,我们不难推论出,netty的轮询注册机制其实是将AbstractNioChannel内部的jdk类SelectableChannel对象注册到jdk类Selctor对象上去,并且将AbstractNioChannel作为SelectableChannel对象的一个attachment附属上,这样再jdk轮询出selector某条SelectableChannel有IO事件发生时,就可以直接取出AbstractNioChannel进行后续操作。

问题:processSelectedKey(SelectionKey k, AbstractNioChannel ch) 逻辑流程?

回答:

(1)对于boss NioEventLoop来说,轮询到的是基本上就是连接事件,后续的事情就通过他的pipeline将连接扔给一个worker NioEventLoop处理;

(2)对于worker NioEventLoop来说,轮询到的基本上都是io读写事件,后续的事情就是通过他的pipeline将读取到的字节流传递给每个channelHandler来处理。

5.2.2.2.2 attachment第二种类型:NioTask
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);

说明注册到selctor上的attachment还有另外一种类型,就是 NioTask,NioTask主要是用于当一个 SelectableChannel 注册到selector的时候,执行一些任务

NioTask的定义

public interface NioTask<C extends SelectableChannel> {
void channelReady(C ch, SelectionKey key) throws Exception;
void channelUnregistered(C ch, Throwable cause) throws Exception;
}

由于NioTask 在netty内部没有使用的地方,这里不过多展开

5.3.3 判断是否该再来次轮询 if(needsToSelectAgain)

if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}

我们回忆一下netty的reactor线程经历前两个步骤,分别是抓取产生过的IO事件以及处理IO事件,每次在抓到IO事件之后,都会将 needsToSelectAgain 重置为false,那么什么时候needsToSelectAgain会重新被设置成true呢?

金手指:NioEventLoop类的run()方法,执行 ​​needsToSelectAgain = false;​

还是和前面一样的思路,我们使用intellij来帮助我们查看needsToSelectAgain被使用的地方,在NioEventLoop类中,只有下面一处将needsToSelectAgain设置为true,就是cancel()方法中

NioEventLoop类的cancel()方法
void cancel(SelectionKey key) {
key.cancel();
cancelledKeys ++;
if (cancelledKeys >= CLEANUP_INTERVAL) {
cancelledKeys = 0;
needsToSelectAgain = true;
}
}

private static final int CLEANUP_INTERVAL = 256;

**继续查看 cancel 函数被调用的地方,在AbstractChannel类中的doDeregister()方法中**
@Override
protected void doDeregister() throws Exception {
eventLoop().cancel(selectionKey());
}

小结:在channel从selector上移除的时候,调用cancel函数将key取消,并且当被去掉的key到达 CLEANUP_INTERVAL 的时候,设置needsToSelectAgain为true,CLEANUP_INTERVAL默认值为256

也就是说,对于每个NioEventLoop而言,每隔256个channel从selector上移除doDeregister()的时候,就标记 needsToSelectAgain 为true,我们还是跳回到上面这段代码

if (needsToSelectAgain) {   // 每满256次,就会进入到if的代码块
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
private void selectAgain() {
needsToSelectAgain = false;
try {
selector.selectNow();
} catch (Throwable t) {
logger.warn("Failed to update SelectionKeys.", t);
}
}

每满256次,就会进入到if (needsToSelectAgain)的代码块,

进入到if (needsToSelectAgain)代码块之后,

首先,for循环将selectedKeys的内部数组全部清空,方便被jvm垃圾回收,

然后,重新调用selectAgain重新填装一下 selectionKey

netty这么做的目的我想应该是每隔256次channel断线,重新清理一下selectionKey,保证现存的SelectionKey及时有效


金手指:介绍一下判断是否该再来次轮询 if(needsToSelectAgain) 的逻辑?
每满256次,就会进入到if (needsToSelectAgain)的代码块,
进入到if (needsToSelectAgain)代码块之后,
首先,for循环将selectedKeys的内部数组全部清空,方便被jvm垃圾回收,
然后,重新调用selectAgain重新填装一下 selectionKey
netty这么做的目的我想应该是每隔256次channel断线,重新清理一下selectionKey,保证现存的SelectionKey及时有效
金手指:关于needsToSelectAgain的设置和读取?
1、needsToSelectAgain 设置为 false:
netty的reactor线程经历前两个步骤,分别是抓取产生过的IO事件以及处理IO事件,每次在抓到IO事件之后,都会将 needsToSelectAgain 重置为false,
NioEventLoop类的run()方法,执行 needsToSelectAgain = false;
2、needsToSelectAgain 设置为 true:
在NioEventLoop类中,只有下面一处将needsToSelectAgain设置为true,就是cancel()方法中
在channel从selector上移除的时候,调用cancel函数将key取消,并且当被去掉的key到达 CLEANUP_INTERVAL 的时候,设置needsToSelectAgain为true,CLEANUP_INTERVAL默认值为256
对于每个NioEventLoop而言,每隔256个channel从selector上移除doDeregister()的时候,就标记 needsToSelectAgain 为true


5.4 小结

netty的reactor线程第二步做的事情为处理IO事件,


  1. netty使用数组替换掉jdk原生的HashSet来保证IO事件的高效处理,
  2. 每个SelectionKey上绑定了netty类AbstractChannel对象作为attachment,在处理每个SelectionKey的时候,就可以找到AbstractChannel,
  3. 然后通过pipeline的方式将处理串行到ChannelHandler,回调到用户方法

六、Reactor线程执行第三步:执行任务队列中的任务

今天,我们要进行的是三部曲中的最后一曲【处理任务队列】,也就是上面图中的紫色部分。

读完本篇文章,你将了解到netty的异步task机制,定时任务的处理逻辑,这些细节可以更好地帮助你写出netty应用

netty中的task的常见使用场景

我们取三种典型的task使用场景来分析


金手指:
Netty中的任务包括两种:非定时任务和定时任务
1、reactor线程调用非定时任务
涉及队列:
2、非reactor线程调用非定时任务/业务线程调用非定时任务
涉及队列:
3、reactor线程调用定时任务
涉及队列:
4、非reactor线程调用定时任务/业务线程调用定时任务
涉及队列:


6.1 铺垫:源码解析:非定时任务的执行过程

6.1.1 reactor线程调用channel的方法

6.1.1.1 reactor线程调用channel的方法:宏观代码Demo

ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
//...
}
});

6.1.1.2 reactor线程调用channel的方法:源码解析

我们跟进execute方法,看重点

@Override
public void execute(Runnable task) {
//...
addTask(task);
//...
}

接上面,进入execute方法调用 addTask方法

protected void addTask(Runnable task) {
// ...
if (!offerTask(task)) {
reject(task);
}
}

接上面,进入offerTask方法,如果offer失败,那就调用reject方法,通过默认的 RejectedExecutionHandler 直接抛出异常

final boolean offerTask(Runnable task) {
// ...
return taskQueue.offer(task);
}

跟到offerTask方法,基本上task就落地了,netty内部使用一个taskQueue将task保存起来,那么这个taskQueue又是何方神圣?且看6.1.3 taskQueue。

6.1.1.3 重要概念:taskQueue本质是一个mpsc队列,多生产者单消费者

我们查看 taskQueue 定义的地方和被初始化的地方

private final Queue<Runnable> taskQueue;

taskQueue = newTaskQueue(this.maxPendingTasks);

@Override
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return PlatformDependent.newMpscQueue(maxPendingTasks); //mpsc队列
}

我们发现 taskQueue在NioEventLoop中默认是mpsc队列,mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便地将外部线程的task聚集,在reactor线程内部用单线程来串行执行,我们可以借鉴netty的任务执行模式来处理类似多线程数据上报,定时聚合的应用

在本节讨论的任务场景中,所有代码的执行都是在reactor线程中的,所以,所有调用 inEventLoop() 的地方都返回true,既然都是在reactor线程中执行,那么其实这里的mpsc队列其实没有发挥真正的作用,mpsc大显身手的地方其实在第二种场景


金手指:
taskQueue在NioEventLoop中默认是mpsc队列,mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便地将外部线程的task聚集,在reactor线程内部用单线程来串行执行,我们可以借鉴netty的任务执行模式来处理类似多线程数据上报,定时聚合的应用。


6.1.2 非reactor线程调用channel的方法

6.1.2.1 非reactor线程调用channel的方法:宏观代码Demo

// non reactor thread
channel.write(...)

上面一种情况在push系统中比较常见,一般在业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景

6.1.2.2 非reactor线程调用channel的方法:源码解析

关于channel.write()类方法的调用链,后面会单独拉出一篇文章来深入剖析,这里,我们只需要知道,最终write方法串至以下方法

AbstractChannelHandlerContext.java

private void write(Object msg, boolean flush, ChannelPromise promise) {
// ...
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}

外部线程在调用write的时候,executor.inEventLoop()会返回false,直接进入到else分支,将write封装成一个WriteTask(这里仅仅是write而没有flush,因此flush参数为false), 然后调用 safeExecute方法

private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) {
// ...
executor.execute(runnable);
// ...
}

接下来的调用链就进入到第一种场景了,但是和第一种场景有个明显的区别就是,第一种场景的调用链的发起线程是reactor线程,第二种场景的调用链的发起线程是用户线程,用户线程可能会有很多个,显然多个线程并发写taskQueue可能出现线程同步问题,于是,这种场景下,netty的mpsc queue就有了用武之地


金手指:
taskQueue是一种mpsc队列,多生产者单消费者队列
(1)当reactor线程调用channel的方法,调用链的发起线程是reactor线程,
(2)当非reactor线程调用channel的方法,调用链的发起线程是用户线程,用户线程可能会有很多个,显然多个线程并发写taskQueue可能出现线程同步问题,于是,这种场景下,netty的mpsc queue就有了用武之地。


6.2 铺垫:源码解析:定时任务的执行过程

6.2.1 用户自定义定时任务:宏观代码到源码解析

ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {

}
}, 60, TimeUnit.SECONDS);

如上代码就是定时任务逻辑了,用的最多的便是如上方法:在一定时间之后执行任务,上面是60s之后执行任务。

我们跟进schedule方法

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
//...
return schedule(new ScheduledFutureTask<Void>(
this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}
<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
// ...
scheduledTaskQueue().add(task);
// ...
return task;
}

小结:定时任务就是在一定时间之后执行任务,三个参数:Runnable对象、延迟时间、延迟的时间单位,将用户自定义任务再次包装成一个netty内部的任务ScheduledFutureTask,然后将这个ScheduledFutureTask放到定时任务队列里面scheduledTaskQueue

6.2.2 重要概念:定时任务队列

6.2.2.1 scheduledTaskQueue()定时任务队列本质是一个PriorityQueue优先级队列

到了这里,我们有点似曾相识,在非定时任务的处理中,netty通过一个mpsc队列将任务落地,这里,是否也有一个类似的队列来承载这类定时任务呢?带着这个疑问,我们继续向前

Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
if (scheduledTaskQueue == null) {
scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
}
return scheduledTaskQueue;
}

果不其然,scheduledTaskQueue() 方法,会返回一个优先级队列,然后调用 add 方法将定时任务加入到队列中去。


金手指:scheduledTaskQueue()定时任务队列本质是一个PriorityQueue优先级队列
相关问题1:定时任务中,这里为什么要使用优先级队列,而不需要考虑多线程的并发?

**回答1:**因为我们现在讨论的场景,调用链的发起方是reactor线程,不会存在多线程并发这些问题。

相关问题2:万一有的用户在reactor之外执行定时任务呢?
**回答2:**虽然这类场景很少见,但是netty作为一个无比健壮的高性能io框架,必须要考虑到这种情况。

对此,netty的处理是,如果是在外部线程调用schedule,netty将添加定时任务的逻辑封装成一个普通的task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对PriorityQueue的访问就变成单线程,即只有reactor线程
ScheduledFuture schedule(final ScheduledFutureTask task) {
if (inEventLoop()) { // 在事件循环里面,表示是reactor线程
scheduledTaskQueue().add(task);
} else { // 不在事件循环里面,表示是外部线程
// 进入到场景二,进一步封装任务
execute(new Runnable() {
@Override
public void run() {
scheduledTaskQueue().add(task); // 执行的时候将定时任务添加到定时任务队列中去
}
});
}
return task;
}


6.2.2.2 定时任务继续深入,定时任务中优先队列的比较逻辑,核心:compareTo方法

在阅读源码细节的过程中,我们应该多问几个为什么?这样会有利于看源码的时候不至于犯困!比如这里,为什么定时任务要保存在优先级队列中,我们可以先不看源码,来思考一下优先级队列的特性

优先队列中的元素之间都是可以比较的优先级队列按一定的顺序来排列内部元素,内部元素必须是可以比较的

现在优先队列中存放定时任务元素,所以这些定时任务之间是可以比较的:联系到这里每个元素都是定时任务,那就说明定时任务是可以比较的,那么到底有哪些地方可以比较?

每个任务都有一个下一次执行的截止时间,截止时间是可以比较的,截止时间相同的情况下,任务添加的顺序也是可以比较的,就像这样,阅读源码的过程中,一定要多和自己对话,多问几个为什么

带着猜想,我们研究与一下ScheduledFutureTask,抽取出关键部分

final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V> {
private static final AtomicLong nextTaskId = new AtomicLong();
private static final long START_TIME = System.nanoTime();

static long nanoTime() {
return System.nanoTime() - START_TIME;
}

private final long id = nextTaskId.getAndIncrement();
/* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */
private final long periodNanos;

@Override
public int compareTo(Delayed o) {
//...
}

// 精简过的代码
@Override
public void run() {
}

这里,我们一眼就找到了compareTo 方法,cmd+u跳转到实现的接口,发现就是Comparable接口

public int compareTo(Delayed o) {
if (this == o) {
return 0;
}

ScheduledFutureTask<?> that = (ScheduledFutureTask<?>) o;
long d = deadlineNanos() - that.deadlineNanos();
if (d < 0) {
return -1;
} else if (d > 0) {
return 1;
} else if (id < that.id) {
return -1;
} else if (id == that.id) {
throw new Error();
} else {
return 1;
}
}

进入到方法体内部,我们发现,两个定时任务的比较,确实是先比较任务的截止时间,截止时间相同的情况下,再比较id,即任务添加的顺序,如果id再相同的话,就抛Error

这样,在执行定时任务的时候,就能保证最近截止时间的任务先执行


金手指:Netty中对于定时任务的优先队列的比较逻辑是怎样的?
1、两个定时任务的比较,确实是先比较任务的截止时间,截止时间相同的情况下,再比较id,即任务添加的顺序,如果id再相同的话,就抛Error
2、这样,在执行定时任务的时候,就能保证最近截止时间的任务先执行


6.2.2.3 定时任务继续深入,Netty三种定时任务

/* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */
private final long periodNanos;

了解这些背景之后,我们来看下netty是如何来处理这三种不同类型的定时任务的

public void run() {
if (periodNanos == 0) { // p=0 若干时间后执行一次,不重复
V result = task.call(); // 执行任务,返回结果
setSuccessInternal(result); // 对结果进行设置
} else {
task.call(); // 执行任务
long p = periodNanos;
if (p > 0) { // p>0 每隔一段时间执行一次
deadlineNanos += p; // 修改截止时间 p为正 加p
} else { // p<0 每次执行结束,隔一定时间再执行一次
deadlineNanos = nanoTime() - p; // 修改截止时间 p为负 减p
}
scheduledTaskQueue.add(this); // 定时任务队列加上这个
}
}
}


金手指:
netty里面的定时任务分以下三种,netty使用一个long类型的 periodNanos变量 来区分三种定时任务


  1. 若干时间后执行一次,不重复 periodNanos = 0
  2. 每隔一段时间执行一次 periodNanos > 0 正数
  3. 每次执行结束,隔一定时间再执行一次 periodNanos < 0 负数

第一种情况,if (periodNanos == 0) 对应 若干时间后执行一次 的定时任务类型,仅仅执行一次,这个任务就结束了,不重复。

否则,进入到else代码块,第一步,执行任务;第二步,区分是哪种类型的任务,periodNanos大于0,表示是重复执行,和执行任务本身的持续时间无关,以固定频率执行某个任务(deadlineNanos
= deadlineNanos + p)
,然后,设置该任务的下一次截止时间为本次的截止时间加上间隔时间periodNanos,否则,就是重复执行,和执行任务本身的持续时间有关,每次任务执行完毕之后,间隔多长时间之后再次执行(deadlineNanos
= nanoTime() - p)
,截止时间为当前时间加上间隔时间,-p就表示加上一个正的间隔时间,最后,将当前任务对象再次加入到队列,实现任务的定时执行


netty内部的任务添加机制了解地差不多之后,我们就可以查看reactor第三部曲是如何来调度这些任务的

6.3 源码:reactor线程task的调度

首先,我们将目光转向最外层的外观代码

runAllTasks(long timeoutNanos);

runAllTasks(long timeoutNanos)

方法概要:尽量在一定的时间内,将所有的任务(非定时任务+定时任务)都取出来run一遍。

方法参数:唯一蚕丝被 timeoutNanos 表示该方法最多执行这么长时间,

相关问题:netty为什么要规定这个所有任务的总执行时间?

回答:reactor线程如果在此停留的时间过长,那么将积攒许多的IO事件无法处理(见reactor线程的前面两个步骤,第一个步骤是轮询IO事件,第二个步骤处理轮询到的IO事件),最终导致大量客户端请求阻塞,因此,默认情况下,netty将控制内部队列的执行时间,这里通过实参规定总执行时间。

方法返回值:boolean,尽量在一定的时间内,将所有的任务(非定时任务+定时任务)都取出来run一遍,返回为true,使用的时候没有变量接收这个参数,略。

好,我们继续跟进

protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
//...

final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}

task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}

afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}

6.3.1 从scheduledTaskQueue转移定时任务到taskQueue(mpsc queue)

private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) { // 不为空一直循环
if (!taskQueue.offer(scheduledTask)) {
// No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
scheduledTask = pollScheduledTask(nanoTime); // (1)首先调用 fetchFromScheduledTaskQueue()方法,将到期的PriorityQueue中的定时任务转移到mpsc queue里面
}
return true;
}
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();

Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
if (scheduledTask == null) {
return null;
}

if (scheduledTask.deadlineNanos() <= nanoTime) {
scheduledTaskQueue.remove();
return scheduledTask;
}
return null;
}

(1)首先调用 fetchFromScheduledTaskQueue()方法,将到期的PriorityQueue中的定时任务转移到mpsc queue里面:可以看到,netty在把任务从scheduledTaskQueue转移到taskQueue的时候还是非常小心的,当taskQueue无法offer的时候,需要把从scheduledTaskQueue里面取出来的任务重新添加回去

从定时任务队列scheduledTaskQueue中拉取一个定时任务的逻辑如下:传入的参数nanoTime为当前时间(其实是当前纳秒减去ScheduledFutureTask类被加载的纳秒个数)

这个实参作为判断依据,每次 pollScheduledTask 的时候,只有在这个定时任务的截止时间已经到了,才会取出来

6.3.2 计算本次任务循环的截止时间

Runnable task = pollTask();
//...
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;

先取出第一个任务,然后用reactor线程传入的超时时间 timeoutNanos 来计算出当前任务循环的deadline,并且使用了runTasks,lastExecutionTime来时刻记录任务的状态。

6.3.3 循环执行任务

for (;;) {
safeExecute(task); // 1、调用safeExecute来确保任务安全执行,忽略任何异常
runTasks ++; // 将已运行任务 runTasks 加一
if ((runTasks & 0x3F) == 0) { // 2、一个判断,每隔0x3F任务,即每执行完64个任务之后,判断当前时间是否超过本次reactor任务循环的截止时间了,如果超过,那就break掉,如果没有超过,那就继续执行
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}

task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
protected static void safeExecute(Runnable task) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception. Task: {}", task, t);
}
}

首先调用safeExecute来确保任务安全执行,忽略任何异常,然后将已运行任务 runTasks 加一,每隔0x3F任务,即每执行完64个任务之后,判断当前时间是否超过本次reactor任务循环的截止时间了,如果超过,那就break掉,如果没有超过,那就继续执行。

相关问题:为什么默认是每执行完64个任务判断是否超过截止时间?

回答:这个netty对性能的优化的一种考虑,假设netty任务队列里面如果有海量小任务,如果每次都要执行完任务都要判断一下是否到截止时间,那么效率是比较低下的。

6.3.4 收尾

afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
@Override
protected void afterRunningAllTasks() {
runAllTasksFrom(tailTasks);
}
public final void executeAfterEventLoopIteration(Runnable task) {
// ...
if (!tailTasks.offer(task)) {
reject(task);
}
//...
}

1、先调用 afterRunningAllTasks 方法,NioEventLoop可以通过父类SingleTheadEventLoop的executeAfterEventLoopIteration方法向tailTasks中添加收尾任务,比如,你想统计一下一次执行一次任务循环花了多长时间就可以调用此方法

**2、this.lastExecutionTime = lastExecutionTime;**简单记录一下任务执行的时间,搜了一下该field的引用,发现这个field并没有使用过,只是每次不停地赋值,赋值,赋值…,改天再去向netty官方提个issue…

6.4 小结


金手指:Reactor线程执行第三步:执行任务队列中的任务


  1. 当前reactor线程调用当前eventLoop执行任务,直接执行,否则,添加到任务队列稍后执行
  2. netty内部的任务分为普通任务和定时任务,分别落地到MpscQueue和PriorityQueue
  3. netty每次执行任务循环之前,会将已经到期的定时任务从PriorityQueue转移到MpscQueue
  4. netty每隔64个任务检查一下是否该退出任务循环


七、面试金手指

7.1 Reactor线程的启动

7.1.1 Netty基础


相关问题1:NioEventLoop里面到底在干些什么事?
回答1:启动Reactor线程和执行Reactor线程。
相关问题2:netty是如何保证事件循环的高效轮询和任务的及时执行?
回答2:Reactor线程执行三个步骤:轮询找到IO事件、处理轮询到的IO事件、执行任务队列中的任务。



Java NIO三要素:channel buffer selector
Reactor三要素:Reactor分发请求,Accept接收,Handler处理读写
netty : ChannelHandler 消息的具体处理器。他负责处理读写操作、客户端连接等事情


7.1.2 Reactor线程的创建与启动


相关问题:源码解析threadFactory.newThread(),使用工厂模式来new一个对象,
为什么是 ThreadPerTaskExecutor 和 DefaultThreadFactory的组合来new一个FastThreadLocalThread?
回答
:标准的netty程序会调用到NioEventLoopGroup的父类MultithreadEventExecutorGroup
MultithreadEventExecutorGroup类构造函数:
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object… args) {
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
}
将初始化的好的ThreadPerTaskExecutor对象,通过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]);
}
小结:newChild()返回一个NioEventLoop类对象,这个NioEventLoop类对象里面放着ThreadPerTaskExecutor对象


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

7.2 Reactor线程执行第一步:轮询IO事件

7.2.1 轮询IO事件:宏观代码Demo


金手指0:reactor线程大概做的事情分为对三个步骤不断循环
注意:三个步骤是循环的,轮询找到IO事件、处理轮序到的IO事件、执行任务队列中的任务(非定时任务和定时任务),三步步骤是循环的,完成了第三步执行任务队列中的任务之后,就继续执行第一步轮询找到IO事件,不断循环,直到没有IO事件了。



金手指0:Reactor线程执行第一步:轮询找到IO事件
第一步,Netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始;
在select(wakenUp.getAndSet(false))
轮询找到IO事件,就是一个for死循环,直到找到IO事件才使用break跳出,找到IO事件跳出包括:
(1)定时任务截止事时间快到了,break跳出for循环,中断本次轮询;
(2)轮询过程中发现有任务加入,break跳出for循环,中断本次轮询;
(3)上面两个步骤可以找到IO事件交个第二第三步骤处理,所以第一步骤先select阻塞下来,阻塞第一个定时任务的截止时间,select()阻塞完成后,后面还有五种跳出轮询的方式
(4)Netty如何处理Java NIO的空转
第二步,跳出select方法的for循环后,唤醒
if (wakenUp.get()) {
selector.wakeup();
} 唤醒


7.2.2 轮询IO事件:源码解析

7.2.2.1 NioEventLoop类select()方法:定时任务截止事时间快到了,break跳出for循环,中断本次轮询


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

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


7.2.2.2 轮询过程中发现有任务加入,break跳出for循环,中断本次轮询


金手指2:轮询过程中发现有任务加入,中断本次轮询
(1)netty为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,表示轮询过程中有任务加入,就执行一次非阻塞select操作(就是selector.selectNow();),跳出循环
(2)注意:cas仅仅提供一个true|false作为线程操作安全的判断条件,这里表示有新线程加入且线程安全条件下,就执行一次非阻塞select操作(就是selector.selectNow();),跳出循环


7.2.2.3 阻塞式select操作,五个条件中只要满足任何一个条件,就会break跳出for循环,中断轮询


金手指3:阻塞式select操作,五个条件中只要满足任何一个条件,就会break跳出for循环,中断轮询

3.1 进入到第三步的两个条件 执行到这一步,说明netty任务队列里面队列为空(如果任务队列不为空,如果轮询过程总有新任务加入,在第二步就会跳出循环),并且所有定时任务延迟时间还未到(大于0.5ms)(如果有一个定时任务的剩余时间小于0.5ms,在第一步就会跳出循环),所以已经轮询找到IO事件交给第二步骤和第三步骤处理了,所以第一步骤轮询找到IO事件这里先阻塞下来,阻塞到第一个定时任务的截止时间,先歇一歇

3.2 第三步开始,

3.2.1 select阻塞(selector.select(timeoutMillis); 这行代码是阻塞,在学习Reactor三种模式的时候就知道了):首先在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间(为什么timeoutMillis是第一个任务的截止时间,因为到这里来的第一个任务的截止时间就阻塞了,后面的无法到这里来)

3.2.2 select阻塞完成后,还有五个条件可以break循环,中断轮询:阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有(只要满足任何一个条件,就会break;跳出for循环,中断轮询)


  1. 轮询到IO事件 (selectedKeys != 0)
  2. oldWakenUp 参数为true (这就是传来进来的实参的唯一作用了,提供一种跳出循环的条件
  3. 任务队列里面有任务(hasTasks)
  4. 第一个定时任务即将要被执行 (hasScheduledTasks())
  5. 用户主动唤醒(wakenUp.get())



相关问题:如果第一个定时任务的延迟非常长,比如一个小时,那么有没有可能线程一直阻塞在select操作? 回答:当然有可能!But,只要在这段时间内,有新任务加入,该阻塞就会被释放。具体解释如下:
// 外部线程调用execute方法添加任务
@Override public void execute(Runnable task) {

wakeup(inEventLoop); // inEventLoop为false

}
// 调用wakeup方法唤醒selector阻塞 protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}
可以看到,在外部线程添加任务的时候,会调用wakeup方法来唤醒selector.select(timeoutMillis)。


7.2.2.4 重点问题:Netty如何解决jdk的nio bug


金手指4:Netty如何解决jdk的nio bug
总述:netty没有从根本上解决jdk的bug,而是通过一种方式来巧妙地避开了这个bug
分述1:netty 会在每次进行 selector.select(timeoutMillis)之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒(这里将time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或许更好理解一些),
如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志,如果持续的时间小于timeoutMillis,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector



分述2:如果持续的时间小于timeoutMillis,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector,介绍一下如何重建selector?
总述 2.1 new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上;
分述 2.2 首先,通过openSelector()方法创建一个newSelector,然后执行一个死循环,只要执行过程中出现过一次并发修改selectionKeys异常,就重新开始转移

具体的转移步骤为


  1. 拿到有效的key(for循环中得到的key)
  2. 取消该key在旧的selector上的事件注册
  3. 将该key对应的channel注册到新的selector上,得到newkey
  4. 重新绑定channel和newkey的关系

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


7.2.3 小结(Reactor线程执行第一步:轮询找到IO事件)


金手指:reactor线程select步骤做的事情?
回答:for循环来实现不断地轮询是否有IO事件发生,并且在轮询的过程中不断检查是否有新任务加入(定时任务和普通任务均可 第二步骤),保证了netty的任务队列中的任务得到有效执行,轮询过程顺带用一个计数器避开了了jdk空轮询的bug(第四步骤),过程清晰明了


7.3 Reactor线程执行第二步:处理轮询到的事件

7.3.1 processSelectedKeys():处理轮询到(就是selected)的事件,该方法有两种情况


金手指:processSelectedKeys():处理轮询到(就是selected)的事件
该方法有两种情况:
如果selectedKeys 不为null,就直接翻转,然后优化执行;
如果selectedKeys 为null,就直接从选择器selector中拿


7.3.2 SelectedSelectionKeySet 类优化


金手指:对于SelectedSelectionKeySet类的解释 总述:SelectedSelectionKeySet类很简单,继承了
AbstractSet,说明该类可以当作一个set来用,但是底层使用两个数组来交替使用,



各个方法1:add()方法(重点)
在add方法中,判断当前使用哪个数组,找到对应的数组,然后经历下面三个步骤


  1. 将SelectionKey塞到该数组的逻辑尾部
  2. 更新该数组的逻辑长度+1
  3. 如果该数组的逻辑长度等于数组的物理长度,就将该数组扩容
    各个方法2:doubleCapacityA()和doubleCapacityB()方法
    就是扩容,没什么好说的
    各个方法3:flip()方法
    先翻转,flip()方法唯一设置isA的地方
    然后返回一个SelectionKey数组元素,如果是isAtrue进来的,这里返回keysA,如果是isAfalse进来的,这里返回keysB
    各个方法4:size()方法
    根据isA返回对应数组的实际容量
    各个方法5:remove() contains() iterator()
    都是无操作



相关问题:介绍一下 SelectedSelectionKeySet 优化?
回答:数组可以使用小标读写元素,时间复杂度为O(1),传统的HashSet读写元素,需要查到元素所在位置,时间复杂度为O(lgn)

以数组代替原生的HashSet,extends AbstractSet,重写 add remove contains size 这些方法,遍历的时候是数组,直接使用数组下标取,如final SelectionKey k = selectedKeys[i];,相对jdk原生的HashSet效率有所提高
我们可以看到,待程序跑过一段时间,等数组的长度足够长,每次在轮询到nio事件的时候,netty只需要O(1)的时间复杂度就能将 SelectionKey 塞到 set中去,而jdk底层使用的hashSet需要O(lgn)的时间复杂度


7.3.3 processSelectedKeys()源码解析

7.3.3.1 取出IO事件以及对应的netty channel类


相关问题:为什么拿到当前SelectionKey之后,将selectedKeys[i]置为null?
回答
:Java四种引用类型,不设置为null就是强引用,GC root可达的,很容易造成gc不掉,内存泄漏就发生了。

想象一下这种场景,假设一个NioEventLoop平均每次轮询出N个IO事件,高峰期轮询出3N个事件,那么selectedKeys的物理长度要大于等于3N,如果每次处理这些key,不置selectedKeys[i]为空,那么高峰期一过,这些保存在数组尾部的selectedKeys[i]对应的SelectionKey将一直无法被回收,SelectionKey对应的对象可能不大,但是要知道,它可是有attachment的,这里的attachment具体是什么下面会讲到,但是有一点我们必须清楚,attachment可能很大,这样一来,这些元素是GC
root可达的,很容易造成gc不掉,内存泄漏就发生了


7.3.3.2 处理该channel(attachment两种类型:AbstractNioChannel)


1.1 注册到selctor上的attachment的第一种类型,就是 AbstractNioChannel
证据:满足条件 if (a instanceof AbstractNioChannel)
相关问题:为什么满足条件 if (a instanceof AbstractNioChannel)
回答:netty的轮询注册机制其实是将AbstractNioChannel内部的jdk类SelectableChannel对象注册到jdk类Selctor对象上去,并且将AbstractNioChannel作为SelectableChannel对象的一个attachment附属上,这样再jdk轮询出selector某条SelectableChannel有IO事件发生时,就可以直接取出AbstractNioChannel进行后续操作
1.2 详细的 processSelectedKey(SelectionKey k, AbstractNioChannel ch) 过程
(1)对于boss NioEventLoop来说,轮询到的是基本上就是连接事件,后续的事情就通过他的pipeline将连接扔给一个worker NioEventLoop处理
(2)对于worker NioEventLoop来说,轮询到的基本上都是io读写事件,后续的事情就是通过他的pipeline将读取到的字节流传递给每个channelHandler来处理



2.1 注册到selctor上的attachment第二种类型,就是 NioTask
证据:NioTask task = (NioTask) a;
强转为NioTask
2.2 NioTask类上的方法
void channelReady(C ch, SelectionKey key) throws Exception;
void channelUnregistered(C ch, Throwable cause) throws Exception;


7.3.3.3 判断是否该再来次轮询 if(needsToSelectAgain)


金手指:介绍一下判断是否该再来次轮询 if(needsToSelectAgain) 的逻辑?
每满256次,就会进入到if (needsToSelectAgain)的代码块,
进入到if (needsToSelectAgain)代码块之后,
首先,for循环将selectedKeys的内部数组全部清空,方便被jvm垃圾回收,
然后,重新调用selectAgain重新填装一下 selectionKey
netty这么做的目的我想应该是每隔256次channel断线,重新清理一下selectionKey,保证现存的SelectionKey及时有效
金手指:关于needsToSelectAgain的设置和读取?
1、needsToSelectAgain 设置为 false:
netty的reactor线程经历前两个步骤,分别是抓取产生过的IO事件以及处理IO事件,每次在抓到IO事件之后,都会将 needsToSelectAgain 重置为false,
NioEventLoop类的run()方法,执行 needsToSelectAgain = false;
2、needsToSelectAgain 设置为 true:
在NioEventLoop类中,只有下面一处将needsToSelectAgain设置为true,就是cancel()方法中
在channel从selector上移除的时候,调用cancel函数将key取消,并且当被去掉的key到达 CLEANUP_INTERVAL 的时候,设置needsToSelectAgain为true,CLEANUP_INTERVAL默认值为256
对于每个NioEventLoop而言,每隔256个channel从selector上移除doDeregister()的时候,就标记 needsToSelectAgain 为true


7.3.4 小结

netty的reactor线程第二步做的事情为处理IO事件,


  1. netty使用数组替换掉jdk原生的HashSet来保证IO事件的高效处理,
  2. 每个SelectionKey上绑定了netty类AbstractChannel对象作为attachment,在处理每个SelectionKey的时候,就可以找到AbstractChannel,
  3. 然后通过pipeline的方式将处理串行到ChannelHandler,回调到用户方法

7.4 Reactor线程执行第三步:执行任务队列中的任务

7.4.1 铺垫:源码解析:非定时任务的执行


金手指:
taskQueue在NioEventLoop中默认是mpsc队列,mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便地将外部线程的task聚集,在reactor线程内部用单线程来串行执行,我们可以借鉴netty的任务执行模式来处理类似多线程数据上报,定时聚合的应用。



金手指:
taskQueue是一种mpsc队列,多生产者单消费者队列
(1)当reactor线程调用channel的方法,调用链的发起线程是reactor线程,
(2)当非reactor线程调用channel的方法,调用链的发起线程是用户线程,用户线程可能会有很多个,显然多个线程并发写taskQueue可能出现线程同步问题,于是,这种场景下,netty的mpsc queue就有了用武之地。


7.4.2 铺垫:源码解析:定时任务的执行


定时任务中,这里为什么要使用优先级队列,而不需要考虑多线程的并发?

因为我们现在讨论的场景,调用链的发起方是reactor线程,不会存在多线程并发这些问题。

但是,万一有的用户在reactor之外执行定时任务呢?虽然这类场景很少见,但是netty作为一个无比健壮的高性能io框架,必须要考虑到这种情况。

对此,netty的处理是,如果是在外部线程调用schedule,netty将添加定时任务的逻辑封装成一个普通的task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对PriorityQueue的访问就变成单线程,即只有reactor线程



金手指:Netty中对于定时任务的优先队列的比较逻辑是怎样的?核心:compareTo方法
1、两个定时任务的比较,确实是先比较任务的截止时间,截止时间相同的情况下,再比较id,即任务添加的顺序,如果id再相同的话,就抛Error
2、这样,在执行定时任务的时候,就能保证最近截止时间的任务先执行



定时任务三分类
netty里面的定时任务分以下三种,netty使用一个long类型的 periodNanos变量 来区分三种定时任务


  1. 若干时间后执行一次,不重复 periodNanos = 0
  2. 每隔一段时间执行一次 periodNanos > 0 正数
  3. 每次执行结束,隔一定时间再执行一次 periodNanos < 0 负数
    定时任务三分类的源码解析
    第一种情况,if (periodNanos == 0) 对应 若干时间后执行一次 的定时任务类型,仅仅执行一次,这个任务就结束了,不重复。
    第二种情况和第三种情况
    否则,进入到else代码块,第一步,执行任务;第二步,区分是哪种类型的任务,periodNanos大于0,表示是重复执行,和执行任务本身的持续时间无关,以固定频率执行某个任务(deadlineNanos = deadlineNanos + p),然后,设置该任务的下一次截止时间为本次的截止时间加上间隔时间periodNanos,否则,就是重复执行,和执行任务本身的持续时间有关,每次任务执行完毕之后,间隔多长时间之后再次执行(deadlineNanos = nanoTime() - p),截止时间为当前时间加上间隔时间,-p就表示加上一个正的间隔时间,最后,将当前任务对象再次加入到队列,实现任务的定时执行


7.4.3 reactor线程task的调度(方法概要+源码解析四步骤)


定时任务就是在一定时间之后执行任务,三个参数:Runnable对象、延迟时间、延迟的时间单位,将用户自定义任务再次包装成一个netty内部的任务ScheduledFutureTask,然后将这个ScheduledFutureTask放到定时任务队列里面scheduledTaskQueue



runAllTasks(long timeoutNanos) 方法概要
方法概要:尽量在一定的时间内,将所有的任务(非定时任务+定时任务)都取出来run一遍。
方法参数:唯一蚕丝被 timeoutNanos 表示该方法最多执行这么长时间,
相关问题:netty为什么要规定这个所有任务的总执行时间?
回答:reactor线程如果在此停留的时间过长,那么将积攒许多的IO事件无法处理(见reactor线程的前面两个步骤,第一个步骤是轮询IO事件,第二个步骤处理轮询到的IO事件),最终导致大量客户端请求阻塞,因此,默认情况下,netty将控制内部队列的执行时间,这里通过实参规定总执行时间。
方法返回值:boolean,尽量在一定的时间内,将所有的任务(非定时任务+定时任务)都取出来run一遍,返回为true,使用的时候没有变量接收这个参数,略。



runAllTasks(long timeoutNanos) 源码解析四步骤
金手指:reactor线程task的调度,源码四步骤:


  1. 从scheduledTaskQueue转移定时任务到taskQueue(mpsc queue)
  2. 计算本次任务循环的截止时间
  3. 执行任务
  4. 收尾
    第一步骤,从scheduledTaskQueue转移定时任务到taskQueue(mpsc queue)
    (1)首先调用 fetchFromScheduledTaskQueue()方法,将到期的PriorityQueue中的定时任务转移到mpsc queue里面:可以看到,netty在把任务从scheduledTaskQueue转移到taskQueue的时候还是非常小心的,当taskQueue无法offer的时候,需要把从scheduledTaskQueue里面取出来的任务重新添加回去
    (2)从定时任务队列scheduledTaskQueue中拉取一个定时任务的逻辑如下:传入的参数nanoTime为当前时间(其实是当前纳秒减去ScheduledFutureTask类被加载的纳秒个数)
    这个实参作为判断依据,每次 pollScheduledTask 的时候,只有在这个定时任务的截止时间已经到了,才会取出来
    第二步骤,计算本次任务循环的截止时间
    先取出第一个任务,然后用reactor线程传入的超时时间 timeoutNanos 来计算出当前任务循环的deadline,并且使用了runTasks,lastExecutionTime来时刻记录任务的状态
    第三步骤,执行任务
    整个放到一个for死循环里面,for (;????,
    循环里面,首先调用safeExecute来确保任务安全执行,忽略任何异常,然后将已运行任务 runTasks 加一,每隔0x3F任务,即每执行完64个任务之后,判断当前时间是否超过本次reactor任务循环的截止时间了,如果超过,那就break掉,如果没有超过,那就继续执行。
    相关问题:为什么默认是每执行完64个任务判断是否超过截止时间?
    回答:这个netty对性能的优化的一种考虑,假设netty任务队列里面如果有海量小任务,如果每次都要执行完任务都要判断一下是否到截止时间,那么效率是比较低下的。

    第四步骤,收尾
    1、先调用 afterRunningAllTasks 方法,NioEventLoop可以通过父类SingleTheadEventLoop的executeAfterEventLoopIteration方法向tailTasks中添加收尾任务,比如,你想统计一下一次执行一次任务循环花了多长时间就可以调用此方法;
    2、this.lastExecutionTime = lastExecutionTime:简单记录一下任务执行的时间,搜了一下该field的引用,发现这个field并没有使用过,只是每次不停地赋值,赋值,赋值…,改天再去向netty官方提个issue…


八、小结

Netty如何封装Reactor,完成了

天天打码,天天进步!!!