上篇文章主要分析了NioEventLoopGroup相关实现,其核心逻辑就是创建一定数量的NioEventLoop线程,本文就为大家分析下NioEventLoop相关实现。
一、NioEventLoop简介
NioEventLoop主要用来开启Selector并处理各种I/O事件,本身其实只完成数据流接入工作,具体的逻辑都是委托给其他类完成的,类图如下:
由于每一个NioEventLoop都是一个线程,因此都会执行自己的run方法,不过在分析其实现之前,先看下其父类SingleThreadEventExecutor实现,二者关系千丝百缕。
二、SingleThreadEventExecutor详解
SingleThreadEventExecutor重写了execute接口,相关源码如下:
@Override
public void execute(Runnable task) {
ObjectUtil.checkNotNull(task, "task");
execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
}
private void execute(Runnable task, boolean immediate) {
boolean inEventLoop = inEventLoop();
// 添加到taskQueue后,在NioEventLoop run方法中进行处理
addTask(task);
if (!inEventLoop) {
// 调用NioEventLoop线程的run方法启动线程,使其可以自旋处理任务
startThread();
if (isShutdown()) {
boolean reject = false;
try {
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException e) {
... ...
}
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && immediate) {
// NioEventLoop的run方法中,自旋的时候如果发现没有任务,会进行阻塞,有新的事件来了之后,再唤醒
wakeup(inEventLoop);
}
}
execute方法主要做了以下几件事:
- addTask将Runnable对象添加到任务队列taskQueue中
- startThread启动该NioEventLoop线程 自旋并处理io任务
- Selector.wakeUp唤醒 select(...)阻塞的worker线程 告诉它有新的感兴趣的事件注册过来了 别睡了要开始干活了
其中,inEventLoop方法很有意思,会有两种情况,第一种:主线程中启动bossGroup线程,bossGroup线程启动workerGroup线程。
三、NioEventLoop详解
分析完其父类,下面就让我们看看NioEventLoop相关实现。
3.1. 开启Selector
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory),
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
NioEventLoop主要是在初始化的时候,通过调用openSelector方法开启selector。通过上面代码可以看到,selector元数据中包含两个属性,分别为selectorTuple.selector和selectorTuple.unwrappedSelector,那么二者有什么区别呢?别着急,让我们继续往下看。查看 openSelector源码如下:
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
// jdk原生selector
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
// 判断是否开启了优化,未开启,则返回jdk原生selector
if (DISABLE_KEY_SET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
// 通过反射创建SelectorImpl对象
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
... ...
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
// 使用优化后的SelectedSelectionKeySet对象替换jdk原生的
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
... ...
// 设置为可访问
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
// 进行字段的替换
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
// 进行替换,并返回selector元数据
selectedKeys = selectedKeySet;
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
查看openSelector源码发现,首先通过jdk原生api,创建了unwrappedSelector对象,如果当前没开启优化开关,则直接返回这个对象;如果开启了优化,则通过反射加载sun.nio.ch.SelectorImpl对象,使用优化后的SelectedSelectionKeySet替换SelectorImpl中的selectedKeys和publicSelectedKeys两个HashSet。 selectedKeys表示就绪key的集合,拥有所有操作事件准备就绪的选择key;publicSelectedKeys为外部防伪就绪key的集合。
最终通过SelectedSelectionKeySetSelector包装java原生的selector对象和优化过后的selectedKeySet,放到selector元数据中。
那么,SelectedSelectionKeySet和jdk原生的集合想必主要做了哪些优化呢?其实就是改变了底层的数据结构,用数组替换了HashSet,重写了add和iterator方法,使得数组的迭代效率更高。
3.2. run方法分析
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
// 当前有任务时,返回selector.selectNow
// 当前无任务时,返回SelectStrategy.SELECT,
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
// 轮训就绪的channel
if (!hasTasks()) {
strategy = select(curDeadlineNanos);
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
default:
}
} catch (IOException e) {
// 当出现异常的时候,需要重新构建selector
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
// 轮训次数++,用来解决jdk空轮训bug
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
try {
// I/O操作,根据selectedKey进行出炉
if (strategy > 0) {
processSelectedKeys();
}
} finally {
// 执行完所有任务
ranTasks = runAllTasks();
}
} else if (strategy > 0) {
final long ioStartTime = System.nanoTime();
try {
// I/O操作,根据selectedKey进行出炉
processSelectedKeys();
} finally {
// 按照一定比例执行任务,可能会遗留一部分任务等待下次执行
final long ioTime = System.nanoTime() - ioStartTime;
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else {
ranTasks = runAllTasks(0);
}
if (ranTasks || strategy > 0) {
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // 解决jdk空轮训bug
selectCnt = 0;
}
} catch {
}
}
}
以上就是run方法主要逻辑,总结来说,run方法主要分为以下三个步骤:
- 通过select方法轮训就绪的channel
- 通过processSelectedKeys方法,处理轮训到的selectionkey
- 通过runAllTasks方法,用来执行队列任务
3.3. select方法分析
run方法中通过轮询调用select方法,看看是否有就绪的channel,轮训的过程中,主要通过调用nio selector的selectNow和select(timeOut)方法,查看源码如下:
private int select(long deadlineNanos) throws IOException {
if (deadlineNanos == NONE) {
return selector.select();
}
// Timeout will only be 0 if deadline is within 5 microsecs
long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}
3.4. processSelectedKeys分析
本方法主要处理select轮训到的就绪key,取出这些SelectionKey以及附件attachment
private void processSelectedKeys() {
// 是否使用优化的key
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
// 原始key处理
processSelectedKeysPlain(selector.selectedKeys());
}
}
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// 设置为null,争取被jvm快速回收
selectedKeys.keys[i] = null;
// 附件
final Object a = k.attachment();
// Netty内部事件
if (a instanceof AbstractNioChannel) {
// 根据key的就绪事件,触发对应的事件方法
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
// 清空i+1之后的selectedKeys
selectedKeys.reset(i + 1);
// 重新选择就绪的key
selectAgain();
i = -1;
}
}
}
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
... ...
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
... ...
unsafe.finishConnect();
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
... ...
ch.unsafe().forceFlush();
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
... ...
}
}
processSelectedKeysPlain方法源码就不贴了,处理方式和processSelectedKeysOptimized类似,区别在于一个是hashSet,一个是数组,遍历方式和remove有点区别。最终调用processSelectedKey进行OP_CONNECT、OP_WRITE、OP_READ、OP_ACCEPT等事件处理
3.5. runAllTasks方法分析
本方法主要用来执行taskQueue队列和定时任务队列中的任务,如心跳检测、异步读写等,NioEventLoop会根据I/O事件与taskQueue运行的时间占比计算任务执行的时常,由于一个NioEventLoop线程会管理很多channel,会对应很多任务,一次性都执行可能执行不完,因此每执行64个任务后就会检测是否到了截止时间,到了就不会继续执行了。
protected boolean runAllTasks(long timeoutNanos) {
// 从定时任务中获取将要执行的任务,丢到taskQueue中
fetchFromScheduledTaskQueue();
// 从taskQueue中获取任务
Runnable task = pollTask();
if (task == null) {
// 执行tailTask中的任务,首位
afterRunningAllTasks();
return false;
}
// 获取执行的截止时间
final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
// 执行task的run方法
safeExecute(task);
runTasks ++;
// 每执行64个任务就进行一次是否到了截止时间检查,防止一次执行任务过多,影响其他I/O事件
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;
}
四、总结
通过上述分析源码分析,想必大家对应NioEventLoop的职责已经很清晰了,简单总结下:
- run方法,通过自旋selector.select(timeoutMillis)实现阻塞,轮训就绪的key
- 当轮训到有就绪的key后,通过processSelectedKeys方法,进行不同的事件处理。
- 同时通过runAllTasks处理taskQueue队列里的任务和其他定时任务等。
其中,唤醒阻塞的方式有两种一是bossGroup接收了新的客户端,将SocketChannel注册到workerGroup的selector之上时,通过selector.wakeUp唤醒;二是selector上感兴趣的事件就绪了有返回值也会唤醒。
NioEventLoop其实还解决了jdk nio空轮训问题,通过维护一个selectCnt变量,当这个变量由于各种异常情况导致大于512时候,会重新构建selector,代码再unexpectedSelectorWakeup方法中,有兴趣的同学可以自行研究下。