Netty 的线程模型是怎么样的?
结论先行:Netty 采用主从 Reactor 多线程模型。
bossGroup只负责 accept 新连接,workerGroup负责已建立连接的读写与事件回调;一个 Channel 从注册起就绑定唯一一个 EventLoop,其生命周期内的所有事件都在同一线程执行,因此业务 Handler 无需加锁——但也因此绝不能在 EventLoop 里做阻塞操作。
一、三层结构
┌──────────────────────────┐
accept ──────>│ bossGroup (1~N 线程) │
│ 只处理 OP_ACCEPT │
└───────────┬──────────────┘
│ register
┌────────────────────┼────────────────────┐
┌────▼─────┐ ┌─────▼────┐ ┌──────▼───┐
│EventLoop1│ │EventLoop2│ │EventLoopN│
│ selector │ │ selector │ │ selector │
│ taskQueue│ │ taskQueue│ │ taskQueue│
└────┬─────┘ └─────┬────┘ └──────┬───┘
Channel A,B Channel C Channel D
(每个 Channel 只属于一个 EventLoop,绑定后不再改变)
二、EventLoop 内部在做什么
// 简化后的 NioEventLoop.run()
for (;;) {
select(); // 轮询就绪事件,带超时避免空转
processSelectedKeys(); // 处理 IO 事件:读、写、accept
runAllTasks(); // 执行任务队列:用户 submit 的任务、定时任务
}
| 组成 | 作用 |
|---|---|
| Selector | 多路复用,一个线程管上千连接 |
| TaskQueue | 跨线程投递任务的入口,ctx.executor().execute() |
| ScheduledTask | schedule() 定时任务,如心跳、超时 |
| 空轮询保护 | 统计 select 次数,异常时重建 Selector,规避 epoll bug |
三、线程数怎么配
// boss 通常 1 个足够,除非要 bind 大量端口
EventLoopGroup boss = new NioEventLoopGroup(1);
// worker 默认 CPU × 2
EventLoopGroup worker = new NioEventLoopGroup();
| 场景 | worker 线程数 | 说明 |
|---|---|---|
| 纯转发、IO 密集 | CPU 数 | 减少上下文切换 |
| 有轻量计算 | CPU × 2 | 默认值 |
| 有阻塞调用 | CPU × 2 + 独立业务线程池 | 阻塞必须移出 EventLoop |
四、把阻塞操作移出 EventLoop
// 错误:在 EventLoop 里查数据库,整条线程上的 Channel 全部卡住
public void channelRead0(ChannelHandlerContext ctx, Request req) {
Result r = jdbc.query(req); // 阻塞 50ms
ctx.writeAndFlush(r);
}
// 正确:交给业务线程池,回调里再写回
private static final ExecutorService BIZ =
new ThreadPoolExecutor(32, 32, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10_000),
new NamedThreadFactory("biz"));
public void channelRead0(ChannelHandlerContext ctx, Request req) {
BIZ.execute(() -> {
Result r = jdbc.query(req);
ctx.writeAndFlush(r); // writeAndFlush 是线程安全的
});
}
也可以用 Netty 自带的 DefaultEventExecutorGroup 给某个 Handler 单独分配线程池:
ch.pipeline().addLast(new DefaultEventExecutorGroup(16), new BusinessHandler());
常见追问
- 追问:一个 Channel 会切换 EventLoop 吗?不会。注册时通过
chooser选定并绑定,之后固定不变,这是“无锁”的前提。 - 追问:
writeAndFlush跨线程安全吗?安全。它会检查当前线程是否为该 Channel 的 EventLoop,不是则封装成任务投递到任务队列。 - 追问:boss 和 worker 能共用一个组吗?可以但不推荐,accept 与读写混在一起会互相影响。
- 追问:
shutdownGracefully为什么有静默期?先停止接收新任务,再给已有任务一段时间执行完,默认 2 秒静默 + 15 秒超时。
小结
- 主从 Reactor:boss 管 accept,worker 管读写,Channel 与 EventLoop 一一绑定。
- 单线程串行带来无锁,也带来“不能阻塞”的硬约束。
- EventLoop 内部是 select → 处理 IO → 跑任务队列的循环。
- 阻塞业务必须移出 EventLoop,用独立线程池或 EventExecutorGroup。