本节摘要:EventLoop 是"单线程 + 事件队列 + 任务队列"的执行体,靠串行化消灭锁;外部线程的交互必须经 execute 投递回它的队列。本节讲清 IO 与任务的配比(ioRatio)、定时任务、以及顺序保证的边界与验证方法。
把 EventLoop 想成一个工人的日程表:循环里只有三件事——查 selector 上有哪些连接就绪、处理这些 IO 事件、消化别人塞进来的任务。伪代码还原它的主循环:
// NioEventLoop.run 的骨架(简化示意) while (!isShutdown()) { selector.select(); // 等事件(含定时任务到期换算的等待时间) processSelectedKeys(); // 处理就绪连接的读写 runAllTasks(timeoutNanos); // 按配比消化任务队列 }
关键设计:IO 事件与普通任务在同一个线程串行执行。好处是处理任何一条连接的数据时,不必担心另一个线程同时在改同一个 Channel 的状态——竞争在架构上不存在,而不是靠锁挡住。

任何非本 EventLoop 线程想动 Channel,都必须走 eventLoop().execute(...),Netty 会把任务塞进队列,由工人在自己的日程里执行:
// 业务线程池算完后,回到连接所属的 EventLoop 写结果 bizExecutor.submit(() -> { String result = slowRpcCall(msg); // 慢活在业务线程做 Channel ch = ctx.channel(); ch.eventLoop().execute(() -> { // 回到本连接的线程 ch.writeAndFlush(result + "\n"); }); });
其实 channel.writeAndFlush 内部就是这么干的:判断当前线程不是本 EventLoop,就自动投递。理解这一层,"为什么在任意线程调 write 是安全的"就有了答案——不是线程安全,是线程归一。
事件处理完才消化任务,任务积压会饿着 IO;反过来只顾 IO,任务队列涨爆。NioEventLoop 用 ioRatio 控制节奏:
// ioRatio 默认 50:一轮里 IO 与任务时间各占一半 ((NioEventLoopGroup) group).setIoRatio(70); // IO 重(转发型服务)调高 // 取值含义:IO 占比。IO 慢任务多时调低,给任务队列留时间
转发型网关(IO 密集、业务极薄)适合调高到 70 甚至 100;业务较重、投递任务频繁的服务保持默认即可。调参前先看第七章仪表盘的任务队列积压指标,盲调没有意义。
EventLoop 自带定时能力,不必引入全局调度器:
EventLoop loop = ctx.channel().eventLoop(); ScheduledFuture<?> f1 = loop.schedule(() -> { System.out.println("10 秒后执行一次"); }, 10, TimeUnit.SECONDS); ScheduledFuture<?> f2 = loop.scheduleAtFixedRate(() -> { System.out.println("每 5 秒一次,且与本连接的 IO 共享同一线程秩序"); }, 0, 5, TimeUnit.SECONDS); f2.cancel(false); // 连接关闭时记得取消,否则线程引用泄漏
定时任务跑在 EventLoop 线程上——任务体必须轻。要跑重活,任务体里只做一件事:把活转投给业务线程池。
顺序的保证范围要划清:同一 Channel 上,IO 事件顺序 = 到达顺序;投递的任务与 IO 之间按入队顺序串行。但两条不同 Channel 之间没有任何顺序保证,即使它们恰好归同一个 EventLoop——到达先后取决于网络。跨连接需要全序时(比如交易系统按全局时间定序),要靠应用层序列号,而不是指望线程调度。
验证工具就是第三章用过的 inEventLoop():
System.out.println(loop.inEventLoop()); // 当前线程是不是本工人 // 在任意回调里打印 true;在 main 或业务线程里打印 false
⚠️ 常见坑:在 EventLoop 线程里调用
Future.get()或await()等待自己队列里的任务——任务排在当前事件后面,永远等不到,形成活锁式卡死(Netty 对部分调用会直接抛 BlockingOperationException,但自定义锁照样卡)。规矩:EventLoop 线程里只允许addListener 风格的异步等待。
💡 关键直觉:EventLoop 的秩序 = 单线程日程表。IO、任务、定时任务都是日程条目,FIFO 执行;外线程交互一律走 execute 正门。把"这段代码在谁的日程上"当成口头禅,并发问题十有八九能秒判。
补一个把"日程表"落到实处的小实验:在 EventLoop 里 schedule 一个 5 秒后执行的空任务,然后在同一连接上发消息——消息处理完全不受影响,因为定时任务被换算成 selector 的等待时长,到点前空隙照样干活。但若把空任务换成 sleep 五秒,整条流水线立刻冻结五秒。"等"由内核代劳就免费,"占着线程等"就是灾难,这两个实验的对照是理解本节的最佳注脚。