Reactor 线程模型深度解析
一、什么是 Reactor 模式?
1.1 Reactor 模式的起源
Reactor 模式是一种事件驱动的设计模式,最早由 Douglas C. Schmidt 在 1995 年提出,用于处理并发的服务请求。
核心思想:
- 使用单个线程监听多个 I/O 事件源
- 当事件发生时,分发给对应的处理器(Handler)
- 避免为每个连接创建一个线程(传统的 Thread-Per-Connection 模型)
1.2 传统的 BIO 模型(Thread-Per-Connection)
在讲 Reactor 之前,我们先看看传统的 BIO 模型是怎么工作的。
BIO 服务器代码:
java
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动,等待连接...");
while (true) {
// 阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("新连接: " + socket.getRemoteSocketAddress());
// 为每个连接创建一个新线程
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buffer = new byte[1024];
int len;
// 阻塞读取数据
while ((len = in.read(buffer)) != -1) {
System.out.println("收到数据: " + new String(buffer, 0, len));
// 回写数据
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start(); // 启动新线程
}
}
}
BIO 模型的工作流程:
客户端1连接 → 创建线程1 → 线程1阻塞读取
客户端2连接 → 创建线程2 → 线程2阻塞读取
客户端3连接 → 创建线程3 → 线程3阻塞读取
...
客户端N连接 → 创建线程N → 线程N阻塞读取
结果:N 个连接 = N 个线程
BIO 模型的问题:
| 问题 | 说明 |
|---|---|
| 线程开销大 | 每个线程占用 1MB 栈内存,1000 个连接 = 1GB 内存 |
| 上下文切换频繁 | 线程越多,CPU 在线程间切换的开销越大 |
| 线程阻塞 | 大部分时间线程都在等待 I/O,CPU 利用率低 |
| 扩展性差 | 无法支持大量并发连接(C10K 问题) |
C10K 问题:如何在单台服务器上同时处理 10,000 个并发连接?BIO 模型无法解决。
二、Reactor 模式的演进
Reactor 模式有三种经典的实现方式,我们逐一分析。
2.1 单 Reactor 单线程模型
这是最简单的 Reactor 模型,所有操作都在一个线程中完成。
架构图:
┌─────────────────────────────────────────────────────┐
│ 单线程 │
│ ┌──────────┐ │
│ │ Reactor │ (Selector) │
│ └────┬─────┘ │
│ │ │
│ ├─ Accept → Acceptor → 建立连接 │
│ ├─ Read → Handler1 → 读取数据 │
│ ├─ Write → Handler2 → 写入数据 │
│ └─ ... │
└─────────────────────────────────────────────────────┘
代码实现:
java
public class SingleReactorSingleThread {
public static void main(String[] args) throws IOException {
// 创建 Selector(Reactor)
Selector selector = Selector.open();
// 创建 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8080));
// 注册 Accept 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("单 Reactor 单线程服务器启动...");
// 事件循环(单线程)
while (true) {
// 阻塞等待事件
selector.select();
// 处理所有就绪的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
// 处理 Accept 事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("接受连接: " + client.getRemoteAddress());
}
// 处理 Read 事件
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = client.read(buffer);
if (len > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到数据: " + new String(data));
// 注册 Write 事件
key.interestOps(SelectionKey.OP_WRITE);
key.attach(data); // 将数据附加到 key
} else if (len == -1) {
// 客户端关闭连接
key.cancel();
client.close();
}
}
// 处理 Write 事件
else if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
byte[] data = (byte[]) key.attachment();
ByteBuffer buffer = ByteBuffer.wrap(data);
client.write(buffer);
// 写完后,重新注册 Read 事件
key.interestOps(SelectionKey.OP_READ);
key.attach(null);
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
}
执行流程:
1. Selector 阻塞等待事件
↓
2. 客户端1连接 → Accept 事件 → Acceptor 处理 → 注册 Read 事件
↓
3. 客户端1发送数据 → Read 事件 → Handler 读取数据 → 业务处理
↓
4. 处理完成 → Write 事件 → Handler 写回数据
↓
5. 回到步骤 1
所有操作都在一个线程中顺序执行
优点:
- 简单,易于实现
- 没有多线程竞争,不需要加锁
- 一个线程可以处理多个连接(解决了 BIO 的线程开销问题)
缺点:
- 无法利用多核 CPU
- 如果某个 Handler 处理时间过长,会阻塞其他所有连接
- 性能瓶颈明显,无法支持高并发
适用场景:
- 连接数少(< 100)
- 业务处理非常快(< 1ms)
- 单核 CPU
2.2 单 Reactor 多线程模型
为了解决单线程模型的性能瓶颈,引入了线程池来处理业务逻辑。
架构图:
┌─────────────────────────────────────────────────────────────┐
│ 主线程 │
│ ┌──────────┐ │
│ │ Reactor │ (Selector) │
│ └────┬─────┘ │
│ │ │
│ ├─ Accept → Acceptor → 建立连接 │
│ ├─ Read → 读取数据 → 提交到线程池 │
│ └─ Write → 写入数据 │
└───────┼───────────────────────────────────────────────────────┘
│
↓ 提交任务
┌─────────────────────────────────────────────────────────────┐
│ 线程池 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Thread 1 │ │ Thread 2 │ │ Thread 3 │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ↓ ↓ ↓ │
│ Handler1 Handler2 Handler3 │
│ (业务处理) (业务处理) (业务处理) │
└─────────────────────────────────────────────────────────────┘
代码实现:
java
public class SingleReactorMultiThread {
// 创建线程池
private static final ExecutorService threadPool =
Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("单 Reactor 多线程服务器启动...");
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
// Accept 在主线程处理
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("接受连接: " + client.getRemoteAddress());
}
else if (key.isReadable()) {
// Read 在主线程读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = client.read(buffer);
if (len > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// 关键:将业务处理提交到线程池
threadPool.submit(() -> {
// 业务处理(可能很耗时)
String result = processData(new String(data));
// 处理完成后,需要写回数据
// 这里有个问题:如何通知主线程写数据?
// 方案1:使用队列
// 方案2:使用 Selector.wakeup()
try {
ByteBuffer writeBuffer = ByteBuffer.wrap(result.getBytes());
client.write(writeBuffer);
} catch (IOException e) {
e.printStackTrace();
}
});
} else if (len == -1) {
key.cancel();
client.close();
}
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
private static String processData(String data) {
// 模拟耗时的业务处理
try {
Thread.sleep(100); // 假设业务处理需要 100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
return "处理结果: " + data;
}
}
执行流程:
1. 主线程 Selector 等待事件
↓
2. 客户端连接 → Accept 事件 → 主线程处理 → 注册 Read 事件
↓
3. 客户端发送数据 → Read 事件 → 主线程读取数据
↓
4. 主线程将数据提交到线程池
↓
5. 工作线程处理业务逻辑(耗时操作)
↓
6. 工作线程写回数据(或通知主线程写)
↓
7. 回到步骤 1
主线程负责 I/O,工作线程负责业务处理
优点:
- 充分利用多核 CPU
- 业务处理不会阻塞 I/O 操作
- 性能比单线程模型好很多
缺点:
- 主线程仍然是单线程,高并发下可能成为瓶颈
- Accept、Read、Write 都在主线程,压力集中
- 多线程编程复杂度增加(需要处理线程间通信)
适用场景:
- 连接数中等(100 - 1000)
- 业务处理比较耗时
- 多核 CPU
2.3 主从 Reactor 多线程模型(Netty 使用的模型)
这是最成熟的 Reactor 模型,也是 Netty 采用的模型。
核心思想:
- 将 Reactor 分为两组:主 Reactor(Boss)和从 Reactor(Worker)
- 主 Reactor 只负责接受连接(Accept)
- 从 Reactor 负责处理 I/O 读写(Read/Write)
- 业务处理可以在从 Reactor 中完成,也可以提交到业务线程池
架构图:
┌─────────────────────────────────────────────────────────────┐
│ 主 Reactor 线程池 (Boss) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Reactor1 │ │ Reactor2 │ (通常只有 1 个) │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ├─ Accept ────┴─ Accept │
│ │ │
│ └─ 将新连接分配给从 Reactor │
└───────┼──────────────────────────────────────────────────────┘
│
↓ 注册到从 Reactor
┌─────────────────────────────────────────────────────────────┐
│ 从 Reactor 线程池 (Worker) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Reactor1 │ │ Reactor2 │ │ Reactor3 │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ├─ Read ├─ Read ├─ Read │
│ ├─ Decode ├─ Decode ├─ Decode │
│ ├─ Process ├─ Process ├─ Process │
│ ├─ Encode ├─ Encode ├─ Encode │
│ └─ Write └─ Write └─ Write │
└─────────────────────────────────────────────────────────────┘
Netty 的实现:
java
public class MasterSlaveReactorServer {
public static void main(String[] args) throws Exception {
// 主 Reactor 线程池(Boss Group)
// 通常只需要 1 个线程,因为 Accept 操作很快
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 从 Reactor 线程池(Worker Group)
// 默认线程数 = CPU 核心数 * 2
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) // 设置主从 Reactor
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 解码器
pipeline.addLast(new StringDecoder());
// 编码器
pipeline.addLast(new StringEncoder());
// 业务处理器
pipeline.addLast(new ServerHandler());
}
});
// 绑定端口
ChannelFuture future = bootstrap.bind(8080).sync();
System.out.println("主从 Reactor 服务器启动,端口: 8080");
// 等待服务器关闭
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
static class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String data = (String) msg;
System.out.println("收到数据: " + data);
// 业务处理
String result = "处理结果: " + data;
// 写回数据
ctx.writeAndFlush(result);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
}
执行流程:
1. 客户端发起连接
↓
2. Boss Reactor 接受连接(Accept)
↓
3. Boss Reactor 将新连接注册到某个 Worker Reactor
↓
4. 客户端发送数据
↓
5. Worker Reactor 检测到 Read 事件
↓
6. Worker Reactor 读取数据 → 解码 → 业务处理 → 编码 → 写回数据
↓
7. 回到步骤 4
Boss 专注于 Accept,Worker 专注于 Read/Write
优点:
- Boss 和 Worker 分离,职责明确
- Boss 不会被 I/O 操作阻塞
- Worker 可以有多个线程,充分利用多核 CPU
- 每个连接绑定到一个 Worker 线程,无需加锁
- 性能最好,可以支持百万级并发
缺点:
- 实现复杂度最高
- 需要处理线程间的任务分配
适用场景:
- 高并发场景(> 1000 连接)
- 需要充分利用多核 CPU
- 对性能要求高的场景
三、三种模型的对比
3.1 性能对比
| 模型 | 线程数 | 并发能力 | CPU 利用率 | 适用场景 |
|---|---|---|---|---|
| 单 Reactor 单线程 | 1 | 低 | 低 | 连接数 < 100 |
| 单 Reactor 多线程 | 1 + N (线程池) | 中 | 中 | 连接数 100-1000 |
| 主从 Reactor 多线程 | M (Boss) + N (Worker) | 高 | 高 | 连接数 > 1000 |
3.2 处理流程对比
单 Reactor 单线程:
Accept → Read → Decode → Process → Encode → Write
└────────────── 全部在一个线程 ──────────────┘
单 Reactor 多线程:
Accept → Read → Decode ┐
└─ 主线程 ─────────────┘
↓ 提交到线程池
Process (工作线程)
↓
Encode → Write ←───────┘
└─ 主线程 ─┘
主从 Reactor 多线程:
Accept (Boss 线程)
↓
注册到 Worker
↓
Read → Decode → Process → Encode → Write
└────────── Worker 线程 ──────────────┘