Netty 第九篇:NioEventLoop.run()方法的执行逻辑
📅 2026-05-27 ✍️ 原创 🏷️ Java / NIO / Netty / 源码分析 / NioEventLoop
在上一篇中,我们讲到 EventLoop 的线程在 doStartThread() 中被启动,随后调用了 SingleThreadEventExecutor.this.run() 方法。
对于 NioEventLoop 来说,这个 run() 方法就是一个死循环。它不仅要处理 IO 事件,还要执行提交过来的各种任务。今天我们就来把这个死循环拆开,看看它到底在忙什么。
一、selectStrategy.calculateStrategy()
run() 方法的第一步,是调用 selectStrategy.calculateStrategy():
protected void run() {
for (;;) {
int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// ...
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
break;
default:
// ...
}
// ...
}
}
calculateStrategy 的逻辑非常直观:
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) {
if (hasTasks) {
// 如果有任务,立即执行一次非阻塞 select
return selectSupplier.get();
} else {
// 否则返回 SELECT,准备阻塞
return SelectStrategy.SELECT;
}
}
这里的 selectSupplier 中执行了 selector.selectNow()方法。
二、selectNow() 与 switch 的跳过
如果执行了 selectNow(),它会返回一个数字(0 到 N),表示当前 Selector 上就绪的 IO 事件数量。
因为返回值不是 SelectStrategy.SELECT,所以 switch 语句会直接走 default 分支,跳过阻塞式的 select,继续执行后面的逻辑。
反之,如果没有任务,calculateStrategy 会返回 SelectStrategy.SELECT,这时才会进入真正的阻塞环节。
三、阻塞 select 的时间计算与无限循环
当进入 select(wakenUp.getAndSet(false)) 时,Netty 会计算一个阻塞超时时间:
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
delayNanos方法在当前EventLoop没有定时任务时,默认返回一秒,如果存在定时任务,返回(最近一个定时任务该执行的时间 - 当前时间)。这是因为EventLoop希望最多阻塞到“最近一个定时任务该执行的时间点”,不能一直阻塞下去,否则定时任务会饿死。
Netty 写了一个无限循环来处理 select,并监控 JDK 空轮询 Bug:
for (;;) {
// 计算剩余的阻塞毫秒数
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
// 如果已经超时,直接跳出循环
if (timeoutMillis <= 0) {
break;
}
// 执行 JDK 的阻塞 select
selector.select(timeoutMillis);
// 检查是否需要退出循环
if (wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
// 检查是否发生空轮询 Bug
if (++selectCnt > minSelectTimeoutCount) {
rebuildSelector();
break;
}
}
这里为什么要 + 500000L(0.5ms)?
这是一个非常细节的优化点。System.nanoTime() 的精度和 selector.select(timeout) 的实现存在微小差异。
- Netty 计算的是精确的纳秒级截止时间。
- JDK 的 select(timeout) 是毫秒级的
在将纳秒转为毫秒时,直接除以1000000会导致小数部分被截断。加上 0.5ms(500000L 纳秒)是为了尽量补偿 JDK 计时精度的误差
因为 JDK 的 NIO 存在一个著名的 Bug:空轮询(Epoll Bug)。在某些场景下,Selector 会立即返回,导致 CPU 100% 空转。
四、空轮询 Bug 与 Selector 重建
Netty 的解决方案非常暴力且有效:
- 记录 select 循环的次数 selectCnt
- 如果在一定时间内(默认 512 毫秒),循环次数超过了阈值(默认 512 次)
- Netty 认为发生了空轮询 Bug
- 调用 rebuildSelector() 重建 Selector
private void rebuildSelector() {
final Selector oldSelector = selector;
final Selector newSelector = openSelector();
// 将旧 Selector 上的 Channel 重新注册到新的 Selector 上
for (SelectionKey key : oldSelector.keys()) {
key.channel().register(newSelector, key.interestOps(), key.attachment());
}
selector = newSelector;
oldSelector.close();
}
五、ioRatio:控制 IO 与任务的时间比例
select 完成后,NioEventLoop就需要处理真正的业务。其中processSelectedKeys方法用于处理IO事件,runAllTasks方法用于处理当前EventLoop上的任务。Netty会需要根据 ioRatio 来决定执行逻辑,用于控制执行IO事件和任务的时间占比。
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
long ioStartTime = System.nanoTime();
processSelectedKeys();
long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
| ioRatio | 行为 |
|---|---|
| 100 | 先处理 IO,再执行所有任务 |
| 50(默认) | IO 和任务各占一半时间 |
| 其他 | 根据 IO 耗时动态限制任务执行时间 |
六、processSelectedKeys 的执行逻辑)
这是 Netty 处理 IO 事件的核心。Netty 为了性能,没有直接使用 JDK 原生的 HashSet 来存储 SelectionKey,而是替换成了优化后的数组(SelectedSelectionKeySet)。
1. 入口:processSelectedKeys()
private void processSelectedKeys() {
if (selectedKeys != null) {
// 优化版:直接遍历数组(默认走这里)
processSelectedKeysOptimized();
} else {
// 原生版:遍历 Set
processSelectedKeysPlain(selector.selectedKeys());
}
}
2. 遍历 SelectionKey 数组
processSelectedKeysOptimized() 会遍历这个数组,并在遍历过程中清空引用以便 GC。
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null; // 置空,帮助 GC
Object a = k.attachment(); // 取出 attachment
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
}
}
}
3. 处理具体的 IO 事件(processSelectedKey)
这是真正的事件分发中心。Netty 会根据 SelectionKey 的 readyOps(就绪事件)进行分类处理:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
int readyOps = k.readyOps();
// 1. 连接事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
ch.unsafe().finishConnect();
}
// 2. 写事件(之前被暂停,现在缓冲区有空间了)
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 3. 读事件 或 接受连接事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
ch.unsafe().read();
}
}
- 如果是 NioServerSocketChannel,这里会触发
OP_ACCEPT,调用unsafe.read()去接收新连接。 - 如果是 NioSocketChannel,这里会触发
OP_READ,调用unsafe.read()去读取数据。
七、runAllTasks 的执行逻辑
IO 事件处理完后,EventLoop 会开始执行积压的任务。这些任务主要分为三类:
| 任务类型 | 说明 |
|---|---|
| 普通任务 | 用户代码提交的任务(如 channel.write()、execute(Runnable)) |
| 定时任务 | 通过 schedule() 提交的延时任务 |
| 收尾任务 | 任务执行完后的清理工作 |
1. 任务队列的结构
Netty 使用 MpscQueue(多生产者单消费者队列) 来保证线程安全。这意味着多个外部线程可以安全地往队列里扔任务,而 EventLoop 线程自己是唯一的消费者。
2. 核心执行逻辑
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue(); // 1. 把到期的定时任务移到普通队列
Runnable task = pollTask(); // 2. 从队列取任务
long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
while (task != null) {
task.run(); // 3. 执行任务
runTasks++;
// 4. 每执行 64 个任务检查一次时间
if ((runTasks & 0x3F) == 0) {
if (deadline <= ScheduledFutureTask.nanoTime()) {
break; // 超时了,停止执行
}
}
task = pollTask(); // 5. 取下一个
}
return runTasks > 0;
}
Netty 不会一次性把所有任务执行完。因为如果有海量的普通任务,会导致 IO 事件迟迟得不到处理(饿死)。所以 Netty 规定:每执行 64 个任务,就检查一次是否超时。如果超时了,哪怕队列里还有任务,也必须停下来,先回去处理 IO 事件。
八、总结
NioEventLoop.run() 的完整闭环如下:
| 步骤 | 核心动作 |
|---|---|
| 1. 策略计算 | 有任务 → selectNow;无任务 → 阻塞 select |
| 2. 防空轮询 | 循环过快 → rebuildSelector() |
| 3. 处理 IO | 遍历 SelectionKey → 根据 readyOps 调用 unsafe.read() / write() |
| 4. 执行任务 | 从 MpscQueue 取任务,每 64 个检查一次时间 |
— 原创文章,转载请注明出处 —

浙公网安备 33010602011771号