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


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 架构认知:面试官不只是想听你说出 “Reactor” 这个词,更想知道你是否清楚 Netty 是怎么把 Reactor 模式落地到代码里的——EventLoopEventLoopGroupBossGroup / WorkerGroup 这几个核心组件各司其职。
  2. 原理深度:考察你对 “一个 Channel 绑定一个 EventLoop、一个 EventLoop 处理多个 Channel” 这种串行化无锁设计的理解,能不能讲清楚为什么 Netty 这么设计。
  3. 生产应用:能不能根据业务场景选择合适的线程模型(单线程 / 多线程 / 主从多线程),以及为什么生产环境绝大多数都是 “主从 Reactor 多线程” 这套组合。

核心答案

一句话先给结论:Netty 的线程模型基于 Reactor 模式,支持三种配置——单线程模型、多线程模型、主从多线程模型(也叫主从 Reactor),生产环境基本都用主从 Reactor。

线程模型 核心结构 适用场景 生产推荐
单线程模型 1 个线程干所有活 连接数极少、QPS 极低 ❌ 不推荐
多线程模型 1 个 Acceptor 线程 + N 个 IO 线程 客户端场景、连接数中等 ⚠️ 视场景
主从 Reactor 多线程 N 个 Boss 线程 + M 个 Worker 线程 服务端、高并发 强烈推荐

注意 Netty 默认的服务端启动代码(两个 EventLoopGroup)就是主从 Reactor 多线程模型。

深度解析

一、Reactor 模式回顾

Netty 的线程模型根源是 Doug Lea 大神的 Reactor 模式。Reactor 模式本质上就是 “事件驱动 + 多路复用 + 线程池” 这三件套。它把 “等待事件” 和 “处理事件” 解耦:

  • Reactor(反应器):负责监听事件(连接、读、写),分发事件
  • Handler(处理器):负责真正处理事件
  • Acceptor:处理新连接的接收

Reactor 模式有三种形态,对应到 Netty 就是上面表格里那三种线程模型。

二、Netty 的主从 Reactor 多线程模型

直接上图,看一张就懂:

Netty 主从 Reactor 多线程模型
Netty 主从 Reactor 多线程模型

整体执行流程我拆开讲:

  • BossGroup 专门负责接收新连接:它内部有若干个 NioEventLoop,轮询 OP_ACCEPT 事件。一旦有新客户端连进来,Boss 线程就从 OS 的全连接队列里 Accept 出新连接(TCP 三次握手由内核完成),把产生的 SocketChannel 注册到 WorkerGroup 里某个 NioEventLoop 的 selector 上。
  • WorkerGroup 负责处理 IO 读写:Worker 线程轮询 OP_READOP_WRITE 事件,数据读到 ByteBuf 后,丢给 ChannelPipeline 里一串 ChannelHandler 处理(编解码、业务逻辑等)。
  • 绑定关系是关键:一个 Channel 一辈子只绑定一个 NioEventLoop,但一个 NioEventLoop 可以绑定多个 Channel。这就保证了单个 Channel 的所有 IO 操作都在同一个线程里完成,天然无锁。

三、NioEventLoop 的内部结构

NioEventLoop 是 Netty 线程模型的真正主角,它本质是一个 “单线程执行器 + Selector + 任务队列” 三合一。

Netty NioEventLoop 内部循环
Netty NioEventLoop 内部循环

NioEventLoop.run() 里的死循环就干三件事:

  • select:调用 selector.select(timeout) 等 IO 事件就绪
  • processSelectedKeys:处理就绪的 IO 事件(读、写、连接)
  • runAllTasks:执行 taskQueue 里的普通任务和 scheduledTaskQueue 里的定时任务

Netty 为了避免 “空轮询 bug”(JDK epoll 100% CPU 那个坑),会在内部检测 select 触发次数,超过阈值(默认 SELECTOR_AUTO_REBUILD_THRESHOLD,512 次)就重建 Selector。

四、串行化无锁设计

这是 Netty 性能炸裂的关键之一,必须讲。

  • 每个 Channel 绑定一个固定的 NioEventLoop
  • 这个 Channel 的所有 IO 操作(read、write、connect)和 ChannelHandler 的回调,全部由绑定的那个 EventLoop 线程执行
  • 既然都在同一个线程里,那 ChannelPipeline 里多个 ChannelHandler 之间共享数据就不需要加锁

换句话说,Netty 用 “绑定 + 串行执行” 这一套把多线程竞争给绕开了。这套设计叫做 “串行化无锁化设计”。当然,如果你的 ChannelHandler 里自己又起了线程池,那就打破这个保证了——所以 Netty 提供了 addLast(EventExecutorGroup, handlers...) 这种重载,允许你把耗时业务逻辑切到独立的业务线程池跑,避免阻塞 IO 线程。

五、三种线程模型在 Netty 里怎么配

代码示例,一看就懂:

// 服务端启动器
EventLoopGroup bossGroup = new NioEventLoopGroup(1);     // Boss 线程数固定 1 即可
EventLoopGroup workerGroup = new NioEventLoopGroup();    // Worker 默认 = CPU 核心数 * 2

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)                  // 传两个 Group —— 主从 Reactor
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             protected void initChannel(SocketChannel ch) {
                 ch.pipeline().addLast(new MyDecoder(), new MyHandler());
             }
         });

三种模型的区别主要就在 group(...) 怎么传:

  • 单线程模型bootstrap.group(group),且 group 只 1 个线程,Boss 和 Worker 共用
  • 多线程模型bootstrap.group(group)group 是多线程的,Boss 和 Worker 共用一个多线程 Group
  • 主从多线程模型bootstrap.group(bossGroup, workerGroup),两个独立的 Group

老规矩:BossGroup 的线程数一般给 1 就够了,因为一个端口只需要一个 ServerSocketChannel 接收连接。WorkerGroup 默认值是 CPU 核心数 * 2,Netty 源码里就是这么算的(MultithreadEventExecutorGroup 默认 nThreads = CPU 核心数 * 2)。

面试高频追问

  1. 追问一:BossGroup 一定要设置成 1 个线程吗?

    不一定。单端口监听的情况下 1 个够用;但如果你监听多个端口(比如同时开 8080 和 8443),BossGroup 线程数就要 >= 2。生产里我一般直接给 1,单端口完全够。

  2. 追问二:为什么 Netty 的 IO 线程不能做耗时操作?

    因为一个 NioEventLoop 上绑定了多个 Channel,IO 线程一旦被某个 Channel 的业务逻辑卡住,其他 Channel 的 IO 事件就全部阻塞了,相当于一个连接拖垮所有连接。解决办法是业务 handler 里把耗时任务丢到独立的业务线程池(DefaultEventExecutorGroup)执行。

  3. 追问三:Netty 怎么解决 JDK 的空轮询 bug?

    通过 NioEventLoop 内部对 select 触发次数做检测,在没超时却提前返回的情况下计数,超过阈值(默认 512)就判定为空轮询,新建一个 Selector 把旧的 Channel 注册信息迁移过去。

常见面试变体

  • “Netty 的 EventLoopEventLoopGroup 是什么关系?”
  • “Netty 主从 Reactor 模式中,Boss 和 Worker 分别负责什么?”
  • “Netty 的 NioEventLoop 内部都做了什么?”
  • “Netty 为什么不用锁?怎么保证线程安全?”

记忆口诀

主从 Reactor 三段式:Boss 接连接 → Worker 干读写 → Channel 绑定 EventLoop 串行跑。

一句话串起来:一个 Channel 一辈子绑定一个 EventLoop,一个 EventLoop 一辈子可以服务多个 Channel。

总结

Netty 的线程模型本质就是 Reactor 模式的工程化落地,主从 Reactor 多线程是生产标配。核心抓住三点就稳:Boss 接连接 / Worker 处理读写 / Channel 与 EventLoop 一对一绑定实现串行无锁。再把 NioEventLoop 的 select → processSelectedKeys → runAllTasks 这条循环说清楚,面试官基本就要点头了。