Java NIO:Reactor 模型与 Netty EventLoop 原理
一、Reactor 模型的基本原理
1. 什么是 Reactor?
Reactor 是一种基于事件驱动的设计模式,主要解决高并发网络通信中的事件监听和分发问题。
先回顾传统 BIO 的处理方式。
假设服务器同时有 1000 个客户端连接,传统 BIO 通常采用一个连接对应一个线程的方式:
客户端 A ──> 线程 A
客户端 B ──> 线程 B
客户端 C ──> 线程 C
...
客户端 N ──> 线程 N这种方式存在几个问题:
连接数量增加,线程数量也随之增加。
大量线程可能阻塞等待网络数据。
线程上下文切换会产生额外开销。
大量线程会占用更多内存。
Reactor 模型的核心思想:
通过 IO 多路复用监听多个 Channel,当某个 Channel 出现 IO 就绪事件时,再分发给对应的处理器处理。
客户端 A ──┐
客户端 B ──┤
客户端 C ──┼──> Reactor
客户端 D ──┤ |
客户端 E ──┘ v
Selector
|
+---------+---------+
| | |
v v v
Channel A Channel B Channel C
| | |
可读 可读 可读Reactor 模型主要包含三个角色:
注意:
Reactor 是一种设计模式,不是 Java 中某个具体的类。
Selector 是 Java NIO 提供的 IO 多路复用组件,可以作为 Reactor 模型的底层实现。
2. Reactor 与 Selector 的关系
前面学习过:
selector.select();Selector 可以同时监听多个 Channel。
当某个 Channel 出现可读、可写等就绪事件时,Selector 会通知程序。
Reactor 在此基础上负责事件分发,将事件交给对应的 Handler 处理。
因此可以理解为:
Selector:负责监听 IO 事件。
Reactor:负责事件分发。
Handler:负责事件处理。
二、Reactor 的三种线程模型
1. 单 Reactor 单线程模型
这是最简单的 Reactor 模型。
一个线程负责:
接收客户端连接。
监听客户端读写事件。
读取网络数据。
处理业务逻辑。
发送响应数据。
结构:
多个客户端
|
v
Reactor + Selector
|
v
Handler
|
v
业务处理与响应优点:
架构简单。
没有多线程竞争问题。
不需要复杂的线程间通信。
缺点:
假设某个客户端触发了耗时业务:
public void handle() throws InterruptedException {
// 模拟耗时操作
Thread.sleep(5000);
// 业务处理
}这 5 秒内,整个 Reactor 线程都被占用了。
其他客户端即使已经有数据到达,也无法及时处理。
因此:
单 Reactor 单线程模型不适合存在大量耗时业务的高并发服务器。
2. 单 Reactor 多线程模型
针对上面的问题,可以将业务处理交给线程池。
Reactor 线程负责:
监听连接。
监听 IO 事件。
读取网络数据。
将业务任务提交给线程池。
Worker 线程负责:
执行业务逻辑。
数据库查询。
远程 HTTP 请求。
其他耗时操作。
结构:
多个客户端
|
v
Reactor + Selector
|
v
读取数据
|
v
业务线程池
/ | \
v v v
Worker Worker Worker
| | |
+-----+-----+
|
v
返回处理结果例如:
ExecutorService executor =
Executors.newFixedThreadPool(10);
// Reactor 线程接收到数据后
executor.execute(() -> {
// Worker 线程执行耗时业务
// 查询数据库
// 调用远程接口
// 执行业务逻辑
});优点:
IO 线程不需要等待耗时业务。
多个 Worker 可以并行处理业务。
提高业务处理能力。
缺点:
Reactor 仍然只有一个,连接接收和 IO 事件处理能力可能成为瓶颈。
需要处理线程间的数据传递。
需要考虑业务执行顺序和并发安全。
3. 主从 Reactor 多线程模型
这是高并发网络服务器常见的架构。
它将 Reactor 分为两类:
MainReactor(主 Reactor)
负责接收客户端连接。
SubReactor(从 Reactor)
负责处理已经建立连接的客户端 IO 事件。
Worker 线程池负责耗时业务。
结构:
多个客户端
|
v
MainReactor
|
accept()
|
+----------+----------+
| | |
v v v
SubReactor SubReactor SubReactor
1 2 3
| | |
v v v
监听IO 监听IO 监听IO
| | |
+----------+----------+
|
v
Worker
线程池
|
v
业务处理整个流程:
客户端发起 TCP 连接。
MainReactor 监听到 OP_ACCEPT。
MainReactor 调用 accept() 接收连接。
将新创建的 SocketChannel 注册到某个 SubReactor。
SubReactor 监听客户端的 OP_READ、OP_WRITE。
发生 IO 事件后,读取和处理网络数据。
耗时业务交给 Worker 线程池。
业务完成后,返回结果并发送响应。
这种方式将连接接收和网络 IO 处理分开,提升了并发处理能力。
重点:MainReactor 主要负责接收连接,SubReactor 主要负责已建立连接的 IO 事件。
三、Netty 与主从 Reactor 模型
理解了三种 Reactor 模型之后,再看 Netty 就容易很多。
Netty 的典型 NIO 服务端线程模型与主从 Reactor 多线程模型非常接近。
Netty 提供了两个核心组件:
EventLoopGroup
EventLoop
1. 什么是 EventLoopGroup?
EventLoopGroup 可以理解为一个 EventLoop 线程组。
例如:
EventLoopGroup bossGroup =
new NioEventLoopGroup(1);
EventLoopGroup workerGroup =
new NioEventLoopGroup();这里:
bossGroup:负责接收客户端连接。
workerGroup:负责处理客户端 IO 事件。
其中:
NioEventLoopGroup 是 Netty 提供的基于 Java NIO 的 EventLoopGroup 实现。
注意:
Netty 不是 Java 8 自带的类库,需要引入第三方依赖。
2. 什么是 EventLoop?
EventLoop 是 Netty 中的事件循环执行单元。
一个 NioEventLoop 通常包含:
一个 IO 线程。
一个 Selector。
一个任务队列。
一个事件循环执行机制。
结构:
NioEventLoop
|
+--------+--------+
| |
v v
Selector Task Queue
| |
v v
监听IO事件 待执行任务
| |
+--------+--------+
|
v
EventLoop
事件循环
|
+--------+--------+
| | |
v v v
处理IO 普通任务 定时任务这里需要理解一个重要区别:
Selector 主要负责监听 IO 事件,而 EventLoop 不仅负责 IO 事件,还负责执行提交到该 EventLoop 的任务。
3. EventLoop 为什么需要任务队列?
因为 EventLoop 不只是处理网络 IO,还需要执行其他任务。
例如:
channel.eventLoop().execute(() -> {
// 提交一个任务给该 Channel 所属的 EventLoop
System.out.println("执行EventLoop任务");
});这个任务会被提交到对应 EventLoop 的任务队列,由 EventLoop 线程执行。
任务队列中可能包含:
普通异步任务。
定时任务。
其他线程提交的任务。
因此:
EventLoop 本质上是一个事件循环执行单元,而不是单纯的 Selector 包装类。
四、一个 EventLoop 为什么可以管理多个 Channel?
这是 Netty 中非常重要的一个知识点。
先看 EventLoopGroup 和 EventLoop 的关系:
NioEventLoopGroup
|
+---- NioEventLoop 1(线程1)
| |
| +---- Selector 1
| +---- Channel A
| +---- Channel B
| +---- Channel C
|
+---- NioEventLoop 2(线程2)
| |
| +---- Selector 2
| +---- Channel D
| +---- Channel E
|
+---- NioEventLoop 3(线程3)
|
+---- Selector 3
+---- Channel F
+---- Channel G你可能会问:
一个线程不是只能处理一个 Channel 吗?
不是。
前面学习过 Selector:
selector.select();一个 Selector 可以监听多个 Channel。
所以:
一个 EventLoop 拥有一个 Selector,就可以通过这个 Selector 管理多个 Channel。
这也是 Netty 能够使用少量 IO 线程管理大量客户端连接的重要原因。
1. Channel 如何分配给 EventLoop?
当一个新的客户端连接建立后,Netty 会从 workerGroup 中选择一个 EventLoop,将客户端 Channel 注册到该 EventLoop。
例如:
客户端 A → EventLoop 1
客户端 B → EventLoop 2
客户端 C → EventLoop 1
客户端 D → EventLoop 3
客户端 E → EventLoop 2Netty 通常通过 EventLoopGroup 的选择器进行分配,默认实现通常采用轮询类的选择策略。
一个 Channel 在生命周期内通常绑定到同一个 EventLoop,不会每次发生 IO 事件就重新分配线程。
2. 为什么一个 Channel 通常只绑定一个 EventLoop?
假设一个客户端连接被多个线程同时处理:
Channel A
|
+---- Thread 1:读取数据
|
+---- Thread 2:修改业务状态
|
+---- Thread 3:发送响应就可能产生线程安全问题。
例如:
多线程同时修改 Channel 对应的业务状态。
多线程处理消息导致顺序混乱。
多线程操作共享数据产生竞争。
Netty 让一个 Channel 通常绑定一个 EventLoop:
Channel A
|
v
EventLoop 1
|
v
Thread 1同一个 Channel 的默认 IO 事件和 Pipeline 处理逻辑通常由同一个线程串行执行。
这样可以减少线程切换,并且降低同一个 Channel 内部发生并发操作的概率。
注意:
这并不意味着整个 Netty 应用不需要线程安全。不同 Channel 可能由不同 EventLoop 同时处理,如果它们访问同一个共享对象,仍然需要考虑线程安全。
五、Netty EventLoop 的工作流程
前面学习 Selector 时,核心流程是:
selector.select();Netty 的 NioEventLoop 在此基础上封装了更完整的事件循环。
可以简化理解为:
while (!terminated) {
// 1. 等待 IO 事件
selector.select();
// 2. 处理已经就绪的 IO 事件
processSelectedKeys();
// 3. 执行普通任务和定时任务
runAllTasks();
}注意:这是帮助理解的简化伪代码。Netty 实际实现还包含任务唤醒、定时任务调度、IO 与任务执行比例控制、Selector 选择策略优化等机制。
1. IO 事件处理
常见事件:
这些事件由 Selector 监听,再由 EventLoop 处理。
2. 普通任务处理
例如:
channel.eventLoop().execute(() -> {
System.out.println(
Thread.currentThread().getName()
);
});任务最终由该 Channel 所属的 EventLoop 执行。
3. 定时任务处理
例如:
channel.eventLoop().schedule(
() -> System.out.println("执行定时任务"),
5,
java.util.concurrent.TimeUnit.SECONDS
);表示提交一个 5 秒后执行的定时任务。
因此:
EventLoop 不仅处理 IO 事件,也负责执行普通任务和定时任务。
六、为什么不能在 Netty IO 线程执行耗时业务?
这是生产环境非常重要的知识点。
假设:
EventLoop 1
|
+---- Channel A
+---- Channel B
+---- Channel C
+---- Channel D这四个客户端都由同一个 EventLoop 线程处理。
如果 Channel A 收到消息后执行:
Thread.sleep(5000);那么这 5 秒内:
Channel B 的 IO 事件无法及时处理。
Channel C 的 IO 事件无法及时处理。
Channel D 的 IO 事件无法及时处理。
因为它们共用同一个 EventLoop 线程。
1. 错误示例
@Override
protected void channelRead0(
ChannelHandlerContext ctx,
String msg) {
// 错误示例:在 IO 线程执行耗时数据库操作
queryDatabase();
ctx.writeAndFlush("处理完成");
}如果 queryDatabase() 阻塞了 3 秒,那么该 EventLoop 管理的其他 Channel 也会受到影响。
2. 推荐方式:使用独立业务线程池
private static final ExecutorService BUSINESS_POOL =
new ThreadPoolExecutor(
10,
20,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Override
protected void channelRead0(
ChannelHandlerContext ctx,
String msg) {
BUSINESS_POOL.execute(() -> {
// 在线程池执行耗时业务
String result = queryDatabase();
// 返回响应
// 从其他线程调用 writeAndFlush 是允许的
// Netty 会处理对应的线程切换
ctx.writeAndFlush(result);
});
}这里使用有界队列,避免业务任务无限堆积。
注意:
CallerRunsPolicy 在队列满时可能让提交任务的线程直接执行任务。如果提交线程是 EventLoop,就可能重新阻塞 IO 线程。
因此,对于 Netty IO 线程,生产环境应根据业务设计合适的拒绝策略,通常需要及时拒绝、限流或返回过载响应,而不是让 IO 线程执行耗时任务。
另外还需要考虑:
线程池队列容量。
线程数。
拒绝策略。
业务执行顺序。
共享对象的线程安全。
业务超时。
3. Netty 还可以使用 DefaultEventExecutorGroup
Netty 自己也提供了业务执行器组。
例如:
EventExecutorGroup businessGroup =
new DefaultEventExecutorGroup(16);注册 Handler 时:
pipeline.addLast(
businessGroup,
new ServerHandler()
);这样 ServerHandler 的事件处理会交给业务执行器组,而不是直接在原来的 IO EventLoop 线程执行。
这是一种常见的业务处理隔离方式。
注意:
同一个 Handler 的事件执行顺序和执行器分配仍然需要理解和管理,不能随意跨线程修改业务状态。
七、Java 8 + Netty 完整示例
通过一个简单的 Netty TCP 服务端,理解 BossGroup、WorkerGroup 和 EventLoop。
1. Maven 依赖
Java 8 自带 NIO、Selector、ByteBuffer。
Netty 是第三方依赖,不属于 Java 8 自带类库。
使用 Netty 4.1 系列 API:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>实际项目建议统一管理 Netty 版本,不要在不同模块中混用版本。
2. 服务端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyServer {
public static void main(String[] args) throws Exception {
// 主线程组:负责接收客户端连接
EventLoopGroup bossGroup =
new NioEventLoopGroup(1);
// 工作线程组:负责客户端 IO 处理
EventLoopGroup workerGroup =
new NioEventLoopGroup();
try {
ServerBootstrap bootstrap =
new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(
ChannelOption.SO_BACKLOG,
1024
)
.childOption(
ChannelOption.TCP_NODELAY,
true
)
.childHandler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline =
ch.pipeline();
// 解析4字节长度字段协议
pipeline.addLast(
new LengthFieldBasedFrameDecoder(
1024,
0,
4,
0,
4
)
);
// 字节转 String
pipeline.addLast(
new StringDecoder(
CharsetUtil.UTF_8
)
);
// String 转字节,并添加4字节长度字段
pipeline.addLast(
new LengthFieldPrepender(4)
);
pipeline.addLast(
new StringEncoder(
CharsetUtil.UTF_8
)
);
// 业务处理器
pipeline.addLast(
new ServerHandler()
);
}
});
// 绑定端口
ChannelFuture future =
bootstrap.bind(8080).sync();
System.out.println("Netty 服务端启动:8080");
// 等待服务端关闭
future.channel().closeFuture().sync();
} finally {
// 优雅关闭线程组
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}3. 业务处理器
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ServerHandler
extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(
ChannelHandlerContext ctx,
String msg) {
System.out.println(
"当前线程:" +
Thread.currentThread().getName()
);
System.out.println("收到客户端消息:" + msg);
// 返回响应
ctx.writeAndFlush("收到:" + msg);
}
@Override
public void channelInactive(ChannelHandlerContext ctx)
throws Exception {
System.out.println("客户端断开连接");
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(
ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}这里使用长度字段编解码器,是为了避免 TCP 半包、粘包问题。
LengthFieldPrepender(4) 负责发送时添加 4 字节长度字段。
LengthFieldBasedFrameDecoder 负责接收时根据长度字段拆分完整消息。
4. 这段代码对应什么线程模型?
核心代码:
bootstrap.group(bossGroup, workerGroup);可以理解为:
客户端连接
|
v
BossGroup
|
v
Boss EventLoop
|
| accept()
v
创建 SocketChannel
|
v
WorkerGroup
|
v
Worker EventLoop
|
v
Selector 监听 IO
|
v
ChannelPipeline
|
v
ServerHandler这里:
BossGroup 接收连接。
WorkerGroup 处理已建立连接的 IO。
每个 Worker EventLoop 可以管理多个客户端 Channel。
每个 Channel 通常绑定一个 EventLoop。
ServerHandler 默认在对应 Channel 的 EventLoop 中执行。
这就是 Netty 典型的主从 Reactor 多线程模型。
八、Selector、EventLoop、EventLoopGroup 的区别
这三个概念必须区分清楚。
结构:
EventLoopGroup
|
+---- EventLoop 1
| |
| +---- Thread 1
| +---- Selector 1
| +---- 多个 Channel
|
+---- EventLoop 2
| |
| +---- Thread 2
| +---- Selector 2
| +---- 多个 Channel
|
+---- EventLoop 3
|
+---- Thread 3
+---- Selector 3
+---- 多个 Channel记忆:
Selector:负责监听 IO。
EventLoop:负责事件循环和任务执行。
EventLoopGroup:负责管理多个 EventLoop。
九、常见问题与答案
1. Reactor 是什么?
Reactor 是一种事件驱动设计模式,通过 IO 多路复用监听多个连接的 IO 事件,并将事件分发给对应处理器处理。
2. Reactor 和 Selector 有什么区别?
Reactor 是设计模式,Selector 是 Java NIO 提供的 IO 多路复用组件。Selector 可以作为 Reactor 的底层事件监听机制。
3. 单 Reactor 单线程有什么缺点?
一个线程同时负责连接接收、IO 事件处理和业务处理。如果业务耗时,就会阻塞其他客户端的事件处理。
4. 单 Reactor 多线程解决了什么问题?
将耗时业务交给线程池,避免业务阻塞 Reactor IO 线程。
5. 主从 Reactor 和单 Reactor 多线程有什么区别?
单 Reactor 多线程通常只有一个 Reactor 负责连接和 IO 事件分发;主从 Reactor 将连接接收和客户端 IO 处理分开,由 MainReactor 和 SubReactor 分别负责。
6. Netty 为什么使用 BossGroup 和 WorkerGroup?
BossGroup 负责接收新连接,WorkerGroup 负责处理已建立连接的 IO 事件,提高连接接收和 IO 处理的扩展能力。
7. 一个 EventLoop 为什么能管理多个 Channel?
因为一个 EventLoop 拥有一个 Selector,而一个 Selector 可以监听多个 Channel。
8. 一个 Channel 通常绑定一个 EventLoop 的主要好处是什么?
让同一个 Channel 的默认事件处理通常串行执行,减少线程切换和并发竞争。
9. EventLoop 和 Selector 有什么区别?
Selector 负责监听 IO 就绪事件;EventLoop 除了处理 IO 事件,还负责执行普通任务和定时任务。
10. 为什么不能在 Netty 的 IO 线程执行耗时业务?
因为一个 EventLoop 可能管理多个 Channel。一个 Channel 的耗时操作会阻塞同一个 EventLoop 中其他 Channel 的事件处理。
11. Netty 的业务线程池和 WorkerGroup 有什么区别?
WorkerGroup 主要负责网络 IO 和事件循环;业务线程池负责执行耗时业务。两者分工不同。
十、重点总结
Reactor 是一种基于事件驱动的设计模式。
Selector 负责监听多个 Channel 的 IO 就绪事件。
单 Reactor 单线程:一个线程负责连接、IO 和业务处理。
单 Reactor 多线程:将耗时业务交给 Worker 线程池。
主从 Reactor 多线程:MainReactor 接收连接,SubReactor 处理客户端 IO。
Netty 的典型 NIO 服务端线程模型与主从 Reactor 模型接近。
BossGroup 负责接收客户端连接。
WorkerGroup 负责处理已建立连接的 IO 事件。
EventLoopGroup 管理多个 EventLoop。
一个 EventLoop 通常对应一个线程和一个 Selector。
一个 EventLoop 可以管理多个 Channel。
一个 Channel 通常绑定一个 EventLoop。
EventLoop 除了处理 IO 事件,还负责普通任务和定时任务。
同一个 Channel 的默认事件处理通常由同一个 EventLoop 串行执行。
不要在 IO 线程执行耗时阻塞业务,应该使用独立业务线程池或 Netty 业务执行器组。
