什么是 Reactor 模式?
Reactor 模式是一种用于处理服务端并发 I/O 的设计模式,它的核心思想是“一个线程(或少量线程)来处理所有 I/O 事件”。

想象一个餐厅的接待员(Reactor),他负责:
- 迎接客人:当有客人(新的连接请求)到来时,他接待。
- 记录客人需求:当客人点菜(I/O 事件,如数据可读/可写)时,他记下来。
- 通知厨师:他不会自己去厨房做菜,而是把记录好的需求单交给专门的厨师(Worker 线程池)去处理。
- 持续巡视:他一直在门口和餐桌之间巡视,随时准备处理新的“事件”(客人到来或客人有新需求)。
这个“接待员”Reactor 的核心,他通过多路复用技术(在 Java 中是 Selector)同时监听多个通道(Channel)上的事件,一旦某个通道有事件发生,他就将这个事件分发给对应的处理器(Handler)去处理。
Reactor 模式的核心组件
一个标准的 Reactor 模式通常包含以下几个关键角色:
-
Reactor (反应器/调度器)
(图片来源网络,侵删)- 职责:核心角色,负责监听和调度事件,它使用一个多路复用器(如
java.nio.channels.Selector)来注册所有感兴趣的 I/O 事件,并在一个独立的线程中不断地轮询这些事件。 - 工作流程:当
Selector检测到某个或多个 Channel 上有事件发生时,Reactor 会将这些事件以及对应的 Channel 封装成一个“事件对象”,然后分发给相应的Handler进行处理。
- 职责:核心角色,负责监听和调度事件,它使用一个多路复用器(如
-
Handler (处理器)
- 职责:负责处理 I/O 事件,每个
Handler通常与一个或多个Channel相关联。 - 分类:
- Acceptor (连接处理器):专门处理新的连接请求,当
Selector检测到ServerSocketChannel上有“接受就绪”事件时,Reactor 会调用Acceptor的accept()方法来创建新的SocketChannel,并为这个新通道注册读事件和一个对应的ReadHandler。 - ReadHandler / WriteHandler (读写处理器):负责处理数据的读取和写入,当
Selector检测到SocketChannel上有“读就绪”或“写就绪”事件时,Reactor 会调用对应的Handler来执行非阻塞的 I/O 操作。
- Acceptor (连接处理器):专门处理新的连接请求,当
- 职责:负责处理 I/O 事件,每个
-
Channel (通道)
- 职责:代表一个开放的连接,可以进行 I/O 操作,在 Java NIO 中,
Channel是双向的,既可以读也可以写,并且可以非阻塞。
- 职责:代表一个开放的连接,可以进行 I/O 操作,在 Java NIO 中,
-
Selector (选择器/多路复用器)
- 职责:Reactor 模式的技术基石,它允许一个线程监视多个
Channel的 I/O 事件。Selector会不断地询问各个Channel是否有事件就绪,这个过程称为“轮询”(Polling)。 - 事件类型:
SelectionKey.OP_ACCEPT(接受连接),SelectionKey.OP_CONNECT(连接完成),SelectionKey.OP_READ(读就绪),SelectionKey.OP_WRITE(写就绪)。
- 职责:Reactor 模式的技术基石,它允许一个线程监视多个
-
Buffer (缓冲区)
(图片来源网络,侵删)- 职责:数据读写的载体,所有 I/O 操作都不是直接与 Channel 交互,而是通过
Buffer进行的,数据从 Channel 读取到 Buffer 中,或者从 Buffer 写入到 Channel 中。
- 职责:数据读写的载体,所有 I/O 操作都不是直接与 Channel 交互,而是通过
Reactor 模式的变种
根据 Reactor 的线程模型,可以分为三种主要的实现方式:
a. 单 Reactor 单线程
- 结构:所有的工作(I/O 事件监听、连接处理、数据读写、业务逻辑处理)都在一个线程中完成。
- 优点:实现简单,没有多线程切换的开销。
- 缺点:性能瓶颈非常明显,任何一步的耗时操作(如复杂的业务逻辑)都会阻塞整个 Reactor 线程,导致后续的 I/O 事件无法被及时处理,无法充分利用多核 CPU。
- 适用场景:低并发、小业务量的场景。
b. 单 Reactor 多线程
- 结构:
- Reactor 线程仍然只负责监听和分发 I/O 事件。
- 当 I/O 事件发生时,Reactor 将任务(通常是
ReadHandler)提交给一个 Worker 线程池。 - Worker 线程池中的线程负责执行数据读取、业务逻辑处理和数据写回。
- 优点:利用了多线程,提高了业务逻辑处理的并行度,解决了单线程模型阻塞的问题。
- 缺点:
- 所有 I/O 操作(如
read(),write())仍然在 Reactor 线程中完成,当 I/O 压力大时,Reactor 线程可能成为瓶颈。 - 需要处理线程安全问题,例如多个线程同时操作同一个
Channel的 Buffer。
- 所有 I/O 操作(如
- 适用场景:I/O 密集型但业务逻辑不算特别复杂的场景。
c. 主从 Reactor 多线程 (Master-Slave Reactor)
这是目前最流行、性能最高的模式,也是 Netty 所采用的模式。
- 结构:
- MainReactor (主 Reactor):通常只有一个线程,它只负责监听服务器端口,处理新连接的
accept事件,当有新连接到来时,它会创建一个SocketChannel,并将其注册到 SubReactor 上。 - SubReactor (从 Reactor):通常有多个,每个
SubReactor运行在一个独立的线程中,它们负责监听已连接的SocketChannel上的 I/O 事件(读、写),当Selector检测到事件后,SubReactor会将其分发给对应的Handler。 - Worker 线程池:
Handler中的耗时业务逻辑(如数据库操作、复杂计算)会被提交给这个线程池异步执行。
- MainReactor (主 Reactor):通常只有一个线程,它只负责监听服务器端口,处理新连接的
- 优点:
- 高并发、高性能:将“连接建立”和“I/O 操作”完全分离开,避免了主 Reactor 成为瓶颈,多个 SubReactor 可以充分利用多核 CPU。
- 职责清晰:模型分工明确,易于扩展和维护。
- 适用场景:高并发、高性能的网络服务,如大型 Web 服务器、消息中间件等。
Java NIO 实现单 Reactor 单线程模型
下面是一个用 Java NIO 实现“单 Reactor 单线程”模型的简单示例,这个例子清晰地展示了 Reactor 模式的核心流程。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 使用 Java NIO 实现 Reactor 单线程模型
*/
public class ReactorSingleThread {
public static void main(String[] args) throws IOException {
// 1. 创建 ServerSocketChannel 并配置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
// 2. 创建 Selector (Reactor 的核心)
Selector selector = Selector.open();
// 3. 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080...");
// 4. Reactor 主循环:轮询并分发事件
while (true) {
// 阻塞等待至少一个通道有就绪事件
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 必须手动移除,否则下次 select 还会处理
iter.remove();
try {
// 5. 事件分发
if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
}
} catch (IOException e) {
// 如果客户端异常断开,关闭 Channel 并取消 SelectionKey
key.cancel();
if (key.channel() instanceof SocketChannel) {
((SocketChannel) key.channel()).close();
}
System.err.println("Client disconnected: " + e.getMessage());
}
}
}
}
// 处理 ACCEPT 事件 (Acceptor 的角色)
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 接受新连接
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
socketChannel.configureBlocking(false);
System.out.println("New client connected: " + socketChannel.getRemoteAddress());
// 将新连接的 SocketChannel 注册到 Selector,监听 READ 事件
// 并将 SocketChannel 作为附件附加到 SelectionKey 上,方便后续处理
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
}
// 处理 READ 事件 (ReadHandler 的角色)
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 从 SelectionKey 获取附件,即之前设置的 Buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
key.cancel();
socketChannel.close();
System.out.println("Client closed connection.");
return;
}
// 切换 Buffer 为读模式,并打印接收到的数据
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("Received from " + socketChannel.getRemoteAddress() + ": " + new String(data));
// 回显数据给客户端
// 注意:这里为了简化,直接在 Reactor 线程中写回,在高性能场景下,
// 这部分操作也应该交给 Worker 线程池。
socketChannel.write(ByteBuffer.wrap(("Echo: " + new String(data)).getBytes()));
// 清空 Buffer,准备下一次读取
buffer.clear();
}
}
代码解析:
- 创建和配置:创建
ServerSocketChannel并设置为非阻塞模式,这是使用 NIO 的前提。 - 创建 Reactor:
Selector就是我们的 Reactor。 - 注册事件:将
ServerSocketChannel注册到Selector,告诉 Reactor 我们关心OP_ACCEPT(新连接)事件。 - 主循环:
while (true)循环是 Reactor 的心跳。selector.select()是阻塞调用,直到有事件发生。 - 事件分发:当
select()返回后,我们遍历所有就绪的SelectionKey,通过isAcceptable()和isReadable()判断事件类型,并调用相应的处理方法。 handleAccept:这是Acceptor,它接受新连接,并将这个新连接的SocketChannel注册到Selector上,监听OP_READ事件,我们创建了一个ByteBuffer作为附件,与这个SocketChannel绑定,用于后续的读写。handleRead:这是ReadHandler,它从SelectionKey中取出SocketChannel和ByteBuffer,读取数据,处理业务逻辑(这里是回显),然后写回数据。注意:这里的业务逻辑和 I/O 操作都在同一个线程中,这是“单线程”模型的体现。
Reactor 模式的优缺点
优点
- 高并发:一个线程可以处理成千上万的连接,极大地减少了线程数量和上下文切换的开销。
- 可扩展性:主从 Reactor 模型可以方便地通过增加 SubReactor 的数量来扩展,充分利用多核 CPU。
- 模型清晰:将 I/O 事件驱动与业务逻辑处理分离,职责明确,代码结构更清晰。
缺点
- 编程复杂:相比传统的阻塞 I/O(BIO),NIO 和 Reactor 模式的编程模型更复杂,需要理解
Selector,Channel,Buffer等概念。 - 依赖操作系统:Reactor 模式的性能高度依赖于操作系统的多路复用实现(如 Linux 的
epoll,Windows 的IOCP)。epoll在处理大量连接时表现优异,而select则有性能瓶颈。 - CPU 竞争:在单 Reactor 模型中,所有任务都在一个线程上,如果业务逻辑非常耗时,可能会阻塞 I/O 事件的处理,导致 CPU 竞争。
| 特性 | 描述 |
|---|---|
| 核心思想 | I/O 多路复用,事件驱动,一个/少量线程处理所有 I/O。 |
| 关键技术 | Java NIO (Selector, Channel, Buffer) |
| 主要模型 | 单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程 |
| 优点 | 高并发、低资源消耗、可扩展性强 |
| 缺点 | 编程模型复杂、对操作系统依赖高 |
| 知名框架 | Netty (实现了主从 Reactor 多线程模型)、Mina |
在现代 Java 高性能网络编程中,Netty 是基于 Reactor 模式(特别是主从多线程模型)最成功和最流行的框架,理解 Reactor 模式是学习和使用 Netty 的基础。
