Netty 线程模型:NIO 之上的工程设计
目录
- 原生 NIO 的问题
- Reactor 模式
- Netty 的主从 Reactor
- EventLoop:调度核心
- ChannelPipeline:流水线
- 为什么能扛百万连接
- 小结
在 Java 服务端开发中,如果需要自己处理大量 TCP 长连接,直接使用 JDK NIO 往往会陷入大量底层细节。Netty 的出现,就是为了把这些复杂的网络编程模型封装起来,让开发者可以更关注业务逻辑。
在说 Netty 之前,需要先把 Java 的三种 IO 模型理一理:
BIO(Blocking IO) 是最传统的 IO 模型。服务端每 accept 一个连接,就创建一个线程去处理这个连接的读写。线程调用 read() 时会阻塞,直到数据到达。这种模型简单直观,但问题也很明显:10 万个连接就需要 10 万个线程,每个线程默认占用 1MB 栈空间,光内存就吃掉 100GB,更别说 CPU 花在上下文切换上的开销。
NIO(Non-blocking IO) 引入了 Selector、Channel、Buffer 三个核心概念。Channel 是非阻塞的,线程调用 read() 时如果没有数据直接返回,不会阻塞。Selector 允许一个线程同时监听多个 Channel 的 IO 事件,哪个 Channel 有数据到达就处理哪个。这就是 IO 多路复用,一个线程就能管理上万个连接。
AIO(Async IO) 更进一步,线程发起 IO 操作后直接返回,操作系统在 IO 完成后通过回调通知应用。理论上 AIO 比 NIO 更高效,但实际上 Java AIO 在不同操作系统上的实现差异较大,在 Linux 下并没有展现出明显优势。Netty 经过长期实践后,选择基于 NIO + epoll 的 Reactor 模型,这套方案在生产环境中更加成熟。
| BIO | 一对一 | 简单但扛不住高并发 |
| NIO | 一对多 | IO 多路复用,一个线程管上万连接 |
| AIO | 零对多(回调) | 理论更优,但 Linux 下不成熟 |
但直接用 Java 原生 NIO 也能写网络程序,为什么还要用 Netty?
原生 NIO 的问题
Java 原生 NIO 提供了 Selector、Channel、Buffer 这套机制,实现了 IO 多路复用。理论上一个线程就能监听多个连接,比 BIO(一个连接一个线程)高效得多。但实际写起来,问题一堆。
// 原生 NIO 的服务端代码骨架
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
} else if (key.isWritable()) {
// 处理写事件
}
}
keys.clear();
}
问题一:代码量大。 一个简单的 echo server 就要写上百行,还得处理各种边界情况。
问题二:粘包半包。 TCP 是流式协议,没有消息边界。客户端发了两个包,服务端可能收到一个半,也可能收到三个。这部分逻辑要自己处理,很繁琐。
问题三:职责不清。 上面的代码里,同一个线程既要监听新连接(OP_ACCEPT),又要处理数据读写(OP_READ/OP_WRITE)。连接数一多,这个线程就成了瓶颈。
Netty 的解决方案就是分工:接连接和处理业务拆开,互不干扰。这就是 Reactor 模式。
Reactor 模式
Reactor 模式解决的问题,是避免线程阻塞等待 IO。它把连接建立、IO 监听、业务处理拆分开,通过事件通知的方式驱动后续处理。
可以把 Reactor 理解为事件调度中心:它监听所有连接的 IO 事件,事件到了就分发给对应的 Handler 处理。

Reactor 模式下,线程不再阻塞在某个连接上等待数据,而是统一监听所有连接,有事件才处理。这种模型下,线程数和连接数解耦,系统资源的利用率大幅提升。
Netty 的主从 Reactor
Netty 在 Reactor 模式的基础上更进一步,用了两组线程: 
BossGroup 的职责比较简单,只负责处理 OP_ACCEPT 事件,也就是接收客户端连接。连接建立后,会被注册到 WorkerGroup 中的某个 EventLoop,由后续线程负责读写事件。
WorkerGroup 负责处理已建立连接的读写事件。每个 EventLoop 管理多个 Channel,通过 Selector 监听 IO 状态,事件发生时调用对应 ChannelPipeline 中的 Handler 处理。
为什么要分两组?因为"接连接"和"处理业务"是两种完全不同的工作。接连接很快,通常微秒级;处理业务可能涉及数据库查询、复杂计算,耗时较长。如果用同一个线程,处理业务的时候就接不了新连接,客户端的连接请求就会被阻塞。
分开了之后,BossGroup 专心接连接,WorkerGroup 专心处理业务,互不影响。
EventLoop:调度核心
EventLoop 是 Netty 线程模型的核心概念。它是一个不断循环的线程,做的事情很简单:监听事件、处理事件、监听事件、处理事件…周而复始。
// EventLoop 的简化伪代码
while (true) {
// 1. 监听 IO 事件,阻塞直到有事件到达
selector.select();
// 2. 处理 IO 事件
for (SelectionKey key : selector.selectedKeys()) {
processIO(key);
}
// 3. 执行任务队列里的任务(比如定时任务、用户提交的任务)
runAllTasks();
}
EventLoop 有几个关键的设计决策:
一个 EventLoop 绑定一个线程。 这个线程从创建到销毁,只属于这一个 EventLoop。不会有多个线程同时操作同一个 EventLoop,所以不需要加锁。
一个 Channel 绑定一个 EventLoop。 一个连接一旦建立,就分配给某个 EventLoop,整个生命周期内不会变。这个 Channel 上所有的 IO 事件都由同一个 EventLoop 处理。
一个 EventLoop 可以管理多个 Channel。 Netty 并不是每个 Channel 创建一个线程,而是让多个 Channel 共享一个 EventLoop。EventLoop 内部通过 Selector 监听这些 Channel 的 IO 状态,当事件发生时,再调用对应 ChannelPipeline 中的 Handler。
EventLoop 1(线程 A)
├── Channel 1(用户 A 的连接)
├── Channel 2(用户 B 的连接)
└── Channel 3(用户 C 的连接)
EventLoop 2(线程 B)
├── Channel 4(用户 D 的连接)
└── Channel 5(用户 E 的连接)
这里也是很多初学者容易误解的地方:EventLoop 并不是一个 Channel 一个线程,而是一个线程管理多个 Channel。Netty 真正高效的地方,不只是用了 NIO,而是利用线程绑定避免了大量并发控制。一个 Channel 的整个生命周期都由同一个 EventLoop 处理,意味着 Handler 里的代码天然就是线程安全的,不需要加锁。
Netty 提供了 NioEventLoopGroup 来创建 EventLoop 组:
// BossGroup:1 个线程,专门接连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// WorkerGroup:默认 CPU 核心数 × 2 个线程,处理业务
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyHandler());
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
这段代码创建了一个 Netty 服务端:BossGroup 用 1 个线程监听 8080 端口,WorkerGroup 用多个线程处理连接上的读写。客户端来了一个连接,BossGroup 接受后把它注册到 WorkerGroup 的某个 EventLoop 上,之后这个连接的所有事件都由那个 EventLoop 处理。
ChannelPipeline:流水线
一个 Channel 上的读写操作,不是由一个 Handler 单独完成的,而是经过一条流水线(Pipeline)。流水线上挂了很多 Handler,每个 Handler 负责一个环节。

流水线分为两个方向:
入站(Inbound):数据从客户端到服务端,比如读数据、解码、业务处理。Handler 从头到尾依次执行。
出站(Outbound):数据从服务端到客户端,比如编码、写数据。Handler 从尾到头依次执行。
// 定义流水线上的 Handler
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4)) // 拆包
.addLast(new StringDecoder()) // 解码
.addLast(new BusinessHandler()) // 业务处理
.addLast(new StringEncoder()) // 编码
.addLast(new LengthFieldPrepender(4)); // 封包
Pipeline 的设计让协议处理和业务逻辑完全解耦。拆包、编解码这些通用逻辑可以复用,开发者只需要关注 BusinessHandler 这一层。
Netty 内置了很多开箱即用的 Handler:
| LengthFieldBasedFrameDecoder | 根据长度字段拆包,解决粘包半包 |
| StringDecoder / StringEncoder | 字符串编解码 |
| IdleStateHandler | 心跳检测,连接空闲超时触发事件 |
| HttpObjectCodec | HTTP 协议编解码 |
| ProtobufDecoder / ProtobufEncoder | Protobuf 编解码 |
你只需要关注业务逻辑,把 BusinessHandler 写好,其他的 Netty 帮你搞定。
为什么能扛百万连接
Netty 能支撑百万级连接,靠的是三个设计决策:
IO 多路复用。 一个线程通过 Selector 监听上万个连接的事件。连接再多,线程数也不用跟着涨。这是百万连接的基础。
主从分离。 接连接和处理业务分开,不会因为业务处理慢而影响新连接的接入。假设一个业务请求处理耗时 10ms,BIO 模型下单个线程每秒最多处理 100 个请求。Netty 用 10 个 Worker 线程,每秒就能处理 1000 个请求。
无锁设计。 一个 Channel 绑定一个 EventLoop,整个生命周期内不需要切换线程。没有锁竞争,没有上下文切换,CPU 的每一分算力都花在处理业务上。
对比一下传统 BIO:
| 连接数 = 线程数 | 是 | 否 |
| 100 万连接需要 | 100 万线程 | 几十个 EventLoop 线程 |
| 内存开销 | 每线程 1MB 栈空间,100万线程 ≈ 1TB | 几十 MB |
| 上下文切换 | 频繁,CPU 浪费严重 | 几乎没有 |
不过需要注意,Netty 能扛百万连接的前提是连接本身没有持续占用 CPU 或大量业务计算。对于大量空闲长连接场景(例如 IM、推送服务),这种模型非常适合。如果每个连接都执行复杂计算,瓶颈仍然会出现在业务线程。
小结
Netty 的线程模型把"接连接"和"处理业务"拆成两组线程(BossGroup 和 WorkerGroup),每组线程由 EventLoop 驱动,一个 EventLoop 管理多个 Channel,Channel 上的处理逻辑通过 Pipeline 串联。分工明确、无锁、线程数可控,这就是它能扛百万连接的原因。
所以理解 Netty,不应该只停留在"它基于 NIO"这一层。NIO 解决的是连接数量问题,而 Netty 真正解决的是工程问题:如何组织线程、管理事件、处理协议、复用组件。这也是它能够长期应用在 RPC、中间件和实时通信系统中的原因。