Netty 的线程模型是怎么样的?

#中间件#面试八股

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()
ScheduledTaskschedule() 定时任务,如心跳、超时
空轮询保护统计 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。
笔记加载中…