在 4.不完全的 NioEventLoopGroup & NioEventLoop 源码分析 文章中, 只是简单的说了一下 NioEventLoop
的继承, 以及实例化过程. 并没有对线程的开启以及任务的执行等有解释, 本篇主要来看这些.
创建 EventLoop 线程
EventLoop 线程的创建是通过调用 SingleThreadEventExecutor#execute
方法, 来创建.
private volatile Thread thread;
private final Queue<Runnable> taskQueue;
private volatile int state = ST_NOT_STARTED;
public void execute(Runnable task) {
ObjectUtil.checkNotNull(task, "task");
execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
}
// 参数 task: 表示要执行的任务.
// 参数 immediate: 表示是否立即执行.
private void execute(Runnable task, Boolean immediate) {
// 判断当前调用 execute 方法的线程, 是否和 thread 变量所保存的是同一对象.
// 如果是则返回 true, 否则返回 false.
Boolean inEventLoop = inEventLoop();
// 将任务添加到 taskQueue 队列.
addTask(task);
if (!inEventLoop) {
// 下面有解释.
startThread();
// state >= ST_SHUTDOWN(4) 说明已经被关闭.
if (isShutdown()) {
Boolean reject = false;
try {
// 任务在关闭之前执行完成, 会返回 false.
// 当移除成功, 说明该任务无法执行, 所以就会抛出下面的异常.
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException e) {
}
// 抛出 RejectedExecutionException("event executor terminated") 异常.
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && immediate) {
// 下面的 解释1.
wakeup(inEventLoop);
}
}
解释1: 要想执行 wakeup
方法, 就必须要 immediate = true
也就是说要立即执行, 但对于 addTaskWakesUp
变量它永远都是 false
.
通过上面的代码可以知道 execute
方法会在当前线程或其它线程中被调用. 而在子类 NioEventLoop
重写了该方法.
protected void wakeup(boolean inEventLoop) {
// inEventLoop 必须要是 false, 保证是在另一个线程中被调用.
// nextWakeupNanos 中保存了超时时间. 也就是说必须要调用了 select 方法.
if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {
// java.nio.channels.Selector
selector.wakeup();
}
}
至于为啥要这样操作, 个人认为当前的 EventLoop
已经调用了 select()
方法并阻塞了, 而另一个 EventLoop
向当前的 EventLoop
中添加了任务, 这个任务又要立即执行, 所以会调用 selector.wakeup()
方法, 让 select()
立即返回, 才能执行该任务.
如果是通过当前的 EventLoop
添加的任务, 就不用调用 selector.wakeup()
方法了, 因为 select()
方法肯定已经返回了.
下面是最主要的 doStartThread()
方法, 对于 startThread
方法无非就是判断线程有没有开启, 修改状态值.
private void startThread() {
// state 变量的默认值为 ST_NOT_STARTED(1).
// 这里就说明还没有开始.
if (state == ST_NOT_STARTED) {
// 通过 CAS 操作, 将 state 变量值改为 ST_STARTED(2), 说明已经开始了.
// 使用 CAS 操作, 是为了防止其它线程已经执行过下面代码了.
// 另外 ST_NOT_STARTED 的值为 1.
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
Boolean success = false;
try {
// 下面有解释.
doStartThread();
success = true;
}
finally {
if (!success) {
// 启动线程失败, 要对状态进行还原.
STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
}
}
}
}
}
private final Executor executor;
private void doStartThread() {
assert thread == null;
// executor 变量, 查看下面 解释2.
executor.execute(new Runnable() {
@Override
// FastThreadLocalThread 类型的线程被启动时, 会执行该方法.
public void run() {
// 这里肯定是新创建的线程执行的该方法, 所以就直接将该线程和当前的 EventLoop 绑定.
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
Boolean success = false;
// 更新上次执行时间.
updateLastExecutionTime();
try {
// 这里执行 run 方法是在子类 NioEventLoop 实现的.
// 该方法是整个 NioEventLoop 核心任务.
SingleThreadEventExecutor.this.run();
success = true;
}
catch (Throwable t) {
// ...
}
finally {
// ...
}
}
}
);
}
解释2: 实例化 NioEventLoop
时, 会调用下面这个父类有参构造方法.
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, Queue<Runnable> taskQueue,
RejectedExecutionHandler rejectedHandler) {
this.executor = ThreadExecutorMap.apply(executor, this);
// ...
}
该构造方法中的参数 executor 的类型为 ThreadPerTaskExecutor
, 至于为什么是这个类型是在 4.不完全的 NioEventLoopGroup & NioEventLoop 源码分析 中有说.
其实也就是调用了, ThreadPerTaskExecutor#execute
在该方法内部会调用 ThreadFactory#newThread
来创建一个线程并启动.
值得注意的是: EventLoop 中的线程都是 FastThreadLocalThread 类型.
NioEventLoop 核心任务
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
/**
* hasTasks(): 如果有未执行的任务就返回 true.
* calculateStrategy: 方法会根据 hasTasks() 返回值,
* 来调用 hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
*
* 注意: selectSupplier.get() 方法最终会调用 selector.selectNow() 方法, 这样就会立即返回.
*/
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
// 根据 calculateStrategy 方法的实现, 该方法只会返回 -1 或 >= 0 的值.
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 获取定时任务的时间(毫秒)
// 只会从 scheduledTaskQueue 队列中获取第一个任务的最后执行时间.
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
// 如果没有定时任务就设置为 Long.MAX_VALUE 值.
curDeadlineNanos = NONE;
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
/**
* 如果没有定时任务则直接调用 selector.select().
* 如果有定时任务则,
* timeoutMillis <= 0 ? selector.selectNow() :
* selector.select(timeoutMillis);
*/
strategy = select(curDeadlineNanos);
}
} finally {
/**
* 这里只是为了防止不必要的 Selector#wakeup 方法的调用.
* 可以看一下上面的 解释1.
*/
nextWakeupNanos.lazySet(AWAKE);
}
default:
}
} catch (IOException e) {
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
// ioRatio 默认值是 50.
final int ioRatio = this.ioRatio;
boolean ranTasks;
// ioRatio 无论是不是 100, 都会先执行 IO 事件以及任务.
if (ioRatio == 100) {
try {
if (strategy > 0) {
processSelectedKeys();
}
} finally {
ranTasks = runAllTasks();
}
} else if (strategy > 0) {
// IO 操作开始时间(纳秒)
final long ioStartTime = System.nanoTime();
try {
// 执行 ChannelHandler
processSelectedKeys();
} finally {
// IO 操作结束时间.
final long ioTime = System.nanoTime() - ioStartTime;
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else { // 没有监听到任何事件就执行任务.
ranTasks = runAllTasks(0);
}
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
// 该方法作用还不清楚
} else if (unexpectedSelectorWakeup(selectCnt)) {
selectCnt = 0;
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
} catch (Throwable t) {
handleLoopException(t);
}
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
ioRatio 无论是不是 100, 都会先执行 IO 事件以及任务. 主要的区别是 taskQueue
任务队列的执行时间, 也就是说不管在这个时间段内队列任务有没有执行完, 都不会再执行了.
值得注意的是:
1.不管任务队列的执行时间是多少, tailTasks 尾部队列的所有任务都会执行.
2.如果说 runAllTasks 方法参数为 0, 那么只会执行 64 个任务后就不会再执行.
processSelectedKeys
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else { // 该变量为 null 只有在 NioEventLoop#openSelector 方法失败后.
processSelectedKeysPlain(selector.selectedKeys());
}
}
private void processSelectedKeysOptimized() {
// 这里是个 for 循环, 会循执行所有的 Channel.
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
// 获取绑定对象, 可以理解为 附件.
// 该值为之前绑定的 NioServerSocketChannel 或 NioSocketChannel.
final Object a = k.attachment();
// AbstractNioChannel 是一个抽象类,
// NioServerSocketChannel 和 NioSocketChannel 都属于该抽象类的实现.
if (a instanceof AbstractNioChannel) {
/**
* 根据 SelectionKey 的事件, 调用 AbstractNioChannel#unsafe() 中的不同方法.
* 例如 OP_READ 或 OP_ACCEPT 都会调用 read 方法.
* 剩下的就可以根据 7. 给大动脉来一刀-ChannelPipeLine, 来看查看事件传播了.
*/
processSelectedKey(k, (AbstractNioChannel) a);
} else {
// 这里没遇到过, 等遇到了再补充.
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
/**
* 什么情况下可以 需要再次选择.
*/
if (needsToSelectAgain) {
// 清空 selectedKeys 数组.
selectedKeys.reset(i + 1);
// 这里再调用一次 selector.selectNow(),
// 保证这段期间没有已准备好的通道了.
selectAgain();
i = -1;
}
}
}
runAllTasks
该方法分为有参和无参, 先来看无参. 只要执行了 taskQueue 队列中的任务该方法就返回 true 否则就返回 false.
看该方法前推荐先看一下方法:
- fetchFromScheduledTaskQueue
- runAllTasksFrom
protected Boolean runAllTasks() {
assert inEventLoop();
Boolean fetchedAll;
Boolean ranAtLeastOne = false;
do {
// 会把所有可执行的计划任务放到任务队列中,
// 直到没有计划任务或没有可以执行的计划任务了,
fetchedAll = fetchFromScheduledTaskQueue();
// 任务队列 中的所有任务.
if (runAllTasksFrom(taskQueue)) {
// 至少运行了一个任务.
ranAtLeastOne = true;
}
} while (!fetchedAll);
// 只要执行了任务就记录一下这次执行时间(System.nanoTime() - START_TIME;).
if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
// 运行 tailTasks(尾部任务) 队列中的任务.
afterRunningAllTasks();
return ranAtLeastOne;
}
fetchFromScheduledTaskQueue
通过方法名知道大概作用就是 从计划任务队列中获取.
private Boolean fetchFromScheduledTaskQueue() {
/**
* 这个就是 计划任务队列 了.
* 先留个坑, 关于该变量的初始化和添加任务后续再说.
*/
if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
return true;
}
// 纳秒的获取最终会调用 ScheduledFutureTask.nanoTime() 方法.
// 最终会使用 当前的纳秒时间 - ScheduledFutureTask 类加载的纳秒时间 得到一个纳秒时间并返回.
// 代码: System.nanoTime() - START_TIME.
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
for (;;) {
// 根据 纳秒值 从 计划任务队列 获取一个可以执行的任务.
Runnable scheduledTask = pollScheduledTask(nanoTime);
if (scheduledTask == null) {
return true;
}
// 将定时任务添加到 任务队列 准备执行.
if (!taskQueue.offer(scheduledTask)) {
// 添加失败就放回 计划任务队列 准备下一次添加.
scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
}
}
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();
// 这个方法就是用来获取 计划任务队列 中的第一个任务的.
ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
// 判断这个任务的执行时间.
// 注意: 如果还没有到执行时间, 这里返回的是 null.
if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) {
return null;
}
// 到了执行之间后才会从 计划任务队列 中移除掉第一个.
scheduledTaskQueue.remove();
// 如果这个任务不是循环的就直接将 deadlineNanos 设置为 0.
scheduledTask.setConsumed();
return scheduledTask;
}
runAllTasksFrom
protected final Boolean runAllTasksFrom(Queue<Runnable> taskQueue) {
// 只要 任务队列 中的任务不是 WAKEUP_TASK 就会返回任务.
// 这里返回 null 是说明 任务队列 没有任务了.
Runnable task = pollTaskFrom(taskQueue);
// 返回 false 表示 没有执行过任务.
if (task == null) {
return false;
}
// 循环执行所有任务.
for (;;) {
// 调用任务的 run 方法.
safeExecute(task);
// 只要 任务队列 中的任务不是 WAKEUP_TASK 就会返回任务.
task = pollTaskFrom(taskQueue);
// 返回 true 表示执行过任务.
if (task == null) {
return true;
}
}
}
接下来在看有参 runAllTasks
方法.
protected Boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
// 从 任务队列 中获取一个不为 WAKEUP_TASK 的任务.
Runnable task = pollTask();
if (task == null) {
// 执行尾部所有任务
afterRunningAllTasks();
return false;
}
// 计算出任务执行时间.
// 公式: (System.nanoTime() - START_TIME) + timeoutNanos.
final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
// 执行任务的 run 方法.
safeExecute(task);
// 运行任务的计数.
runTasks ++;
// 执行完 64 个任务后, 重新计算当前时间.
if ((runTasks & 0x3F) == 0) {
// 定时任务初始化到当前的时间
lastExecutionTime = ScheduledFutureTask.nanoTime();
// 如果超过预定时间则不执行(nanoTime() 是耗时的)
if (lastExecutionTime >= deadline) {
break;
}
}
// 从 任务队列 中获取一个不为 WAKEUP_TASK 的任务.
task = pollTask();
// 没有任务可以执行了后就记录这次执行时间(System.nanoTime() - START_TIME;).
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
// 执行尾部所有任务
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
execute
execute 只能接受 Runnable 类型的任务, 而且该方法没有返回值.
bossGroup.execute(new Runnable() {
@Override
public void run() {
System.out.println("Asdasdad");
}
});
或者
bossGroup.execute(() -> {
System.out.println("Asdasdad");
});
submit
submit 不管是 Runnable 还是Callable 类型的任务都可以接受, 但是 Runnable 类型返回值可以为 void 或通过参数指定返回值类型, 如果没有返回值则 Future 的 get() 获得的还是 null.
Future<Object> submit = bossGroup.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println("Asdasdad");
return null;
}
});
或者
Future<Object> submit = bossGroup.submit(() -> {
System.out.println("Asdasdad");
return null;
});
Runnable
还是 Callable
都会封装为 RunnableFuture
类型, 然后执行 execute
方法.
schedule(计划任务)
ScheduledFuture<String> schedule = bossGroup.schedule(new Callable() {
@Override
public Object call() throws Exception {
return "dsfdfs";
}
}, 10, TimeUnit.SECONDS);
会将当前的 Eventloop
和 要执行的任务, 以及要执行任务的启动时间(毫秒值), 封装成 ScheduledFutureTask
; 然后调用 schedule
方法, 代码如下.
private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
// 如果是在同一个线程中添加
if (inEventLoop()) {
/**
* 该方法会先初始化 scheduledTaskQueue 队列, 然后给任务设置ID并将任务添加到 scheduledTaskQueue 队列.
* 该队列是 DefaultPriorityQueue 实例, 根据执行时间保证队列元素的顺序性.
*
* 注意: 同一个线程中添加计划任务时, 就算添加任务的时间小于当前任务的
*/
scheduleFromEventLoop(task);
} else {
final long deadlineNanos = task.deadlineNanos();
/**
* 当前任务结束时间 < 下次任务的结束时间, 则就会将当前任务添加到 taskQueue 队列.
*
* 下次任务的执行时间是根据 nextScheduledTaskDeadlineNanos() 方法获取的.
*/
if (beforeScheduledTaskSubmitted(deadlineNanos)) {
// 把当前任务添加到 taskQueue 队列, 就是为了能让该任务立即执行.
execute(task);
} else {
/**
* 该方法也是将任务添加到 taskQueue 队列, 但并不是立即执行.
*/
lazyExecute(task);
/**
* 该方法与 beforeScheduledTaskSubmitted 方法一样, 都是怕当前任务的执行时间
* 晚于下次任务的执行时间, 所以要立马执行.
*/
if (afterScheduledTaskSubmitted(deadlineNanos)) {
execute(WAKEUP_TASK);
}
}
}
return task;
}
该方法添加的计划任务并不像 HashedWheelTimer
一样, 该计划任务由于使用的是 EventLoop
执行, 所以有可能会影响 IO 事件的执行.
其实这三种添加任务的方式, 无论哪一种只要任务执行时间长, 都会影响 IO 事件的执行.
executeAfterEventLoopIteration(添加尾部任务)
// 该任务会被添加到 tailTasks 队列.
SingleThreadEventLoop singleThreadEventLoop = ch.eventLoop();
singleThreadEventLoop.executeAfterEventLoopIteration(new Runnable() {
@Override
public void run() {
System.out.println("别看我第一个添加, 但是我最后一个执行.");
}
});
注意: 虽然是尾部任务, 但是当 ioRatio
不为 100
时, 有可能 taskQueue
队列中的任务没有执行完还是会执行 tailTasks
队列中的任务. 但是 tailTasks
队列中的任务都会被执行完.
参考资料
Java NIO的wakeup剖析
Java NIO系列教程(六) Selector
Netty 源码解析(七): NioEventLoop 工作流程
Netty源码学习(九) NioEventLoop的启动
Netty Reactor线程启动及执行
Netty NioEventLoop 啟動過程原始碼分析
Netty 之工作线程 NioEventLoop
NioEventLoop篇
Netty NioEventLoop 启动过程源码分析
深入理解 NioEventLoop启动流程
Netty-NioEventLoop源码剖析