Netty 第四篇:Reactor 模型与 parentGroup / childGroup
上一篇我们拆解了 NioEventLoopGroup 的初始化过程,知道了它内部创建了一组 NioEventLoop, 每个 NioEventLoop 就是一个"线程 + Selector + 任务队列"的组合。
但不知道你有没有注意到一个细节:在 Netty 服务端启动代码里,我们总是写——
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(...);
为什么这里要传两个 EventLoopGroup?一个不行吗?为什么第一个参数通常只配 1 个线程?
这一篇,我们就来把这些问题彻底讲清楚。
一、从 ServerBootstrap.group() 说起
先看一下我们上一篇留下的服务端代码:
package demo.nio;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyServer {
public static void main(String[] args) throws Exception {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[5];
byteBuf.readBytes(bytes);
System.out.println(new String(bytes));
super.channelRead(ctx, msg);
}
});
}
})
.bind(8080)
.sync();
}
}
注意 .group(new NioEventLoopGroup(1), new NioEventLoopGroup()) 这一行—— 这里实际上调用的是 ServerBootstrap 自己的 group(parentGroup, childGroup) 方法。
我们来看一下它的源码:
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
// 管理客户端 Channel 的线程组
private volatile EventLoopGroup childGroup;
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
// 调用父类方法,设置 parentGroup
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
}
再看看父类 AbstractBootstrap 中的 group 字段:
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> {
// 管理服务端 ServerChannel 的线程组
volatile EventLoopGroup group;
public B group(EventLoopGroup group) {
this.group = ObjectUtil.checkNotNull(group, "group");
return self();
}
}
所以结论很清楚:
parentGroup(第一个参数)→ 赋值给父类 → 管理ServerSocketChannelchildGroup(第二个参数)→ 赋值给ServerBootstrap自身 → 管理客户端SocketChannel
一句话理解:parentGroup 管"进门",childGroup 管"干活"。
二、Reactor 设计模式
要理解为什么需要两个 Group,就不得不提 Reactor 设计模式。
Reactor 模式的核心思想是:将"等待事件"和"处理事件"分开,由一个专门的线程负责监听事件, 事件就绪后再分发给对应的处理线程。
Netty 用的是 Reactor 模式的主从多线程变体,一共涉及两类角色:
2.1 主 Reactor(Main Reactor)
- 负责监听服务端端口
- 处理客户端的 连接请求(Accept 事件)
- 将建立好的连接"分发"给从 Reactor
2.2 从 Reactor(Sub Reactor)
- 负责监听已建立连接的 Channel
- 处理 Channel 上的 读 / 写事件(IO 操作)
- 执行 ChannelPipeline 中的 Handler 逻辑
对应到 Netty 的代码里:
| Reactor 角色 | Netty 中的对应 | 职责 | 线程数建议 |
|---|---|---|---|
| 主 Reactor | parentGroup(bossGroup) | Accept 新连接 | 通常为 1 |
| 从 Reactor | childGroup(workerGroup) | 处理 IO 事件 | CPU 核数 × 2 |
.group(new NioEventLoopGroup())), 那么 parentGroup 和 childGroup 会共用同一个 Group,这就退化成了"单 Reactor 多线程"模型。三、为什么 parentGroup 通常只需要 1 个线程?
这是面试中非常高频的一个问题。我们先看代码中的写法:
new NioEventLoopGroup(1) // parentGroup,只配 1 个线程
为什么是 1?难道不会因为只有一个线程处理连接而成为瓶颈吗?
答案是:不会,原因有以下几点。
3.1 Accept 操作非常轻量
ServerSocketChannel.accept() 做的事情非常简单:从内核的已完成三次握手队列中取一个连接, 创建一个 SocketChannel,然后把它注册到 childGroup 中的某个 NioEventLoop 上。
整个过程是纯内存操作,不涉及任何 IO 读写,速度极快。 一个线程每秒可以轻松处理数万个连接请求,远远超过实际场景的需求。
3.2 JDK Selector 本身就不支持多线程
这是更深层的原因。Selector.select() 方法是阻塞的, 而且同一时刻只能有一个线程调用它。即使你在 parentGroup 里配了多个线程, 同一时间也只有一个线程能监听端口上的 Accept 事件。
换句话说,多配线程也没用,因为底层 Selector 的特性决定了"监听端口"这个动作天生就是单线程的。
3.3 多端口场景才需要多个线程
如果你的服务端需要同时监听多个端口(比如 8080、9090、3306),那么每个端口的 Accept 操作是独立的, 这时才需要考虑给 parentGroup 配置多于 1 个的线程。但绝大多数场景,一个端口就够了。
3.4 源码中的注释也证实了这一点
Netty 源码中 ServerBootstrap 的 JavaDoc 明确写道:
The parentEventLoopGroupis used to handle all the incoming connections. The childEventLoopGroupis used to handle the traffic of the accepted connections.
而在实际使用中,parentGroup 的线程数设为 1 是业界共识(Netty 官方示例、RocketMQ、Dubbo 等无不如此)。
四、如果只用一个 Group 会怎样?
我们来做个实验,把服务端代码改成这样:
NioEventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(group) // 只传一个 Group
.channel(NioServerSocketChannel.class)
.childHandler(...);
这时候会发生什么?
- parentGroup 和 childGroup 指向同一个 NioEventLoopGroup
- Accept 事件和 IO 事件混在同一个线程池里处理
- 如果某个连接的 IO 操作阻塞了,会影响其他连接的 Accept
虽然 Netty 仍然能跑,但你已经失去了"主从分离"带来的好处:
| 对比维度 | parentGroup + childGroup 分离 | 只用一个 Group |
|---|---|---|
| 职责分离 | ✅ Accept 与 IO 互不影响 | ❌ 混在一起 |
| 线程隔离 | ✅ 一个线程挂了不影响另一个 | ❌ 无隔离 |
| 调优灵活度 | ✅ 可分别调线程数 | ❌ 只能统一调 |
| 适用场景 | ✅ 生产环境推荐 | ⚠️ 仅适合demo或极低并发 |
结论:能用两个 Group 就别用一个,这是 Netty 官方推荐的做法。
五、完整流程:从 bind 到 Accept 到 Register
最后,我们把整个流程串起来,看看一个连接从进入到被处理,经历了什么:
- 创建 Group:parentGroup(1 个线程)和 childGroup(CPU×2 个线程)初始化完毕
- 创建 ServerSocketChannel:在 parentGroup 的 NioEventLoop 上注册
- bind(8080):端口绑定,开始监听
- 客户端连接:parentGroup 的 Selector 检测到 Accept 事件
- Accept:创建 SocketChannel,注册到 childGroup 中某个 NioEventLoop 的 Selector 上
- IO 处理:childGroup 的 NioEventLoop 轮询 Selector,处理 Read/Write 事件
可以用一张表来总结:
| 组件 | 角色 | 线程数 | 管理的 Channel |
|---|---|---|---|
| parentGroup(boss) | 门卫 | 1 | ServerSocketChannel |
| childGroup(worker) | 服务员 | CPU × 2 | 所有客户端 SocketChannel |
六、总结
这一篇我们围绕 ServerBootstrap.group(parentGroup, childGroup) 展开了讨论,核心要点如下:
- parentGroup 管理 ServerSocketChannel,负责 Accept 新连接。
- childGroup 管理客户端 SocketChannel,负责处理 IO 事件。
- 这种分工本质上是 Reactor 主从模式在 Netty 中的实现。
- parentGroup 只需要 1 个线程,因为 Accept 操作极轻量,且 JDK Selector 本身不支持多线程并发监听。
- 生产环境一定要分开配置,获得职责隔离和更好的可维护性。
— 原创文章,转载请注明出处 —

浙公网安备 33010602011771号