文章目录
- [Java NIO前置知识](#Java NIO前置知识)
-
- Buffer的基本使用
- Selector各操作系统的实现
- Selector其他知识
-
- wakeup的作用
-
- [深入理解 wakeup() 的行为](#深入理解 wakeup() 的行为)
- [为什么需要使用 wakeup()?(常见应用场景)](#为什么需要使用 wakeup()?(常见应用场景))
-
- [场景一:跨线程注册新 Channel](#场景一:跨线程注册新 Channel)
- 场景二:优雅地关闭服务器
- 场景三:跨线程修改监听事件 (Interest Ops)
- [Java实现Multi-Reactor 模式](#Java实现Multi-Reactor 模式)
-
- 设计
-
- 核心组件映射 (C vs Java)
- 架构运行流程设计
-
- [1. Main Reactor (主接受者)](#1. Main Reactor (主接受者))
- [2. Sub Reactor (IO 搬运工)](#2. Sub Reactor (IO 搬运工))
- [3. Worker 线程池 (业务算力)](#3. Worker 线程池 (业务算力))
- [关键差异点:关于 LT 和 ET 模式](#关键差异点:关于 LT 和 ET 模式)
- 实现
- 其他框架使用NIO的示例
-
- [Apache HttpClient4](#Apache HttpClient4)
- [Kafka Java Client](#Kafka Java Client)
- 结语
上一篇文章介绍了C语言中通过epoll实现Multi-Reactor 模式,这篇文章用Java实现。有了上一篇博文的知识背景,其实在Java中写起对应的代码时比较顺利的,只需要在Java中找到对应的类即可
Java NIO前置知识
建议阅读这个博主有关NIO系列博文,说的简短又清楚
NIO中涉及了几个组件:Channel、Buffer、Selector
Typically, all IO in NIO starts with a Channel. A Channel is a bit like a stream. From the Channel data can be read into a Buffer. Data can also be written from a Buffer into a Channel. Here is an illustration of that
Buffer的基本使用
When you write data into a buffer, the buffer keeps track of how much data you have written. Once you need to read the data, you need to switch the buffer from writing mode into reading mode using the flip() method call. In reading mode the buffer lets you read all the data written into the buffer.
Once you have read all the data, you need to clear the buffer, to make it ready for writing again. You can do this in two ways: By calling clear() or by calling compact(). The clear() method clears the whole buffer. The compact() method only clears the data which you have already read. Any unread data is moved to the beginning of the buffer, and data will now be written into the buffer after the unread data
Selector各操作系统的实现
对于Selector接口
- Linux系统的实现是EPollSelectorImpl,最终会调用epoll
- Windows系统的实现是WindowsSelectorImpl,最终会调用select
- Mac系统的实现是KQueueSelectorImpl,最终会调用kqueue
Selector其他知识
wakeup的作用
简单来说,其作用是唤醒正在阻塞等待的 Selector
selector.select() 这个方法默认情况下,如果没有任何通道准备好事件,调用这个方法的线程就会一直阻塞(休眠)在那里,什么也不做。
但在实际复杂的应用中,我们有时需要打断这种"休眠"。
深入理解 wakeup() 的行为
wakeup() 有以下几个特点:
- 正在阻塞时唤醒: 如果当前有一个线程正阻塞在
selector.select()或selector.select(timeout)方法上,此时另一个线程调用了selector.wakeup(),那么那个阻塞的线程会立刻返回,不再继续等待。 - 提前"存下"一次唤醒: 如果当前没有 线程阻塞在
select()上,此时你调用了wakeup(),那么下一次 调用select()的线程将会立即返回,而不会阻塞。这就好像wakeup()提前发放了一张"免等待通行证"。
为什么需要使用 wakeup()?(常见应用场景)
通常,在单线程的简单模型中,可能很少需要用到 wakeup()。但只要引入了多线程,它就变得必不可少了。以下是三个最常见的场景:
场景一:跨线程注册新 Channel
Selector 的内部操作涉及到一些锁机制。如果一个线程(线程 A)正阻塞在 selector.select() 上,而另一个线程(线程 B)想要将一个新的 SocketChannel 注册到这个 Selector 上:
- 线程 B 调用
channel.register(selector, ...) - 此时,线程 B 需要调用
selector.wakeup()唤醒线程 A让其感知到新注册的channel
场景二:优雅地关闭服务器
当你想要停止服务器时,事件循环线程可能正阻塞在 select() 方法里。
java
// 负责事件循环的线程
while (isRunning) {
selector.select(); // 如果没有事件,线程会卡在这里
// ... 处理事件
}
// 另一个控制线程想要关闭服务器
public void stopServer() {
isRunning = false;
selector.wakeup(); // 强行唤醒事件循环线程,让它检查 isRunning 变为 false,从而优雅退出循环
}
场景三:跨线程修改监听事件 (Interest Ops)
有时候,一个在后台处理数据的线程发现数据准备好发送了,它需要将某个 Channel 的关注事件从"只读"改为"读和写"。修改完事件后,通常需要调用 wakeup() 唤醒 Selector,让它能立刻感知到事件设定的变化并开始监听写事件
Java实现Multi-Reactor 模式
Multi-Reactor 模式是什么见上一篇博文
设计
核心组件映射 (C vs Java)
| C 语言组件 | Java NIO 对应组件 | 说明 |
|---|---|---|
epoll_fd |
Selector |
Java 的多路复用器。主 Reactor 和每个子 Reactor 都会拥有自己独立的一个 Selector。 |
server_fd (监听) |
ServerSocketChannel |
专门用来 Accept 新连接的通道。 |
client_fd (连接) |
SocketChannel |
客户端的连接通道,设置为非阻塞模式 (configureBlocking(false))。 |
eventfd (唤醒铃铛) |
selector.wakeup() |
只要调用这个方法,阻塞在 select() 上的线程就会立刻醒来,不需要我们手动建管道。 |
fd_queue + mutex |
ConcurrentLinkedQueue |
线程安全的无锁队列。主线程把 SocketChannel 塞进这里,子线程安全地取出来。 |
epoll_event.data |
SelectionKey.attachment() |
在 Java 中,我们可以给每个连接挂载一个附件(比如 Connection 上下文、Buffer),非常方便。 |
架构运行流程设计
整体依然是经典的三层架构:1 个 Main Reactor + N 个 Sub Reactor + 业务线程池。
1. Main Reactor (主接受者)
- 职责 :单线程运行,持有一个专属的
Selector,只注册OP_ACCEPT事件。 - 动作:
- 当有新连接到达时,调用
serverChannel.accept()拿到SocketChannel。 - 将其设置为非阻塞。
- 通过轮询(Round-Robin)选出一个 Sub Reactor。
- 把这个
SocketChannel放进那个 Sub Reactor 的无锁队列中。 - 关键一步 :调用目标 Sub Reactor 的
selector.wakeup()将其唤醒。
2. Sub Reactor (IO 搬运工)
- 职责 :N 个线程,每个线程有专属的
Selector,负责处理OP_READ和OP_WRITE。 - 动作:
- 处理新连接 :被唤醒后,首先检查自己的无锁队列。如果有主线程塞进来的新
SocketChannel,就拿出来,注册到自己的Selector上,监听OP_READ。 - 读取数据 :发现
OP_READ就绪后,使用ByteBuffer读取数据。读完后,将数据和SocketChannel包装成一个 Task,扔给 Worker 线程池。 - 发送数据 :发现
OP_WRITE就绪后,将挂载在通道上的ByteBuffer(我们自己设计的输出缓冲区)发送出去。发完后取消OP_WRITE监听。
3. Worker 线程池 (业务算力)
- 职责 :专职计算业务逻辑。我们可以直接使用 Java 自带的
ExecutorService(比如ThreadPoolExecutor)。 - 动作:
- 从任务队列拿到数据,进行耗时计算(模拟)。
- 计算完成后,将结果写入该连接的"输出缓冲区"。
- 修改该连接在
Selector上的监听状态,增加OP_WRITE。 - 再次调用所属 Sub Reactor 的
selector.wakeup(),通知 IO 线程去发数据。
关键差异点:关于 LT 和 ET 模式
在架构设计上,Java NIO 和 C 语言有一个极其关键的区别:
- Java NIO 的
Selector默认且只能是 LT (水平触发) 模式。 - 标准 JDK 没有直接暴露底层 OS 的 ET 模式开关(只有极少数特定平台或通过 Netty 的 Native 库才能开启 ET)。
假设在注册通道时,直接写了 channel.register(selector, OP_READ | OP_WRITE)。结果程序运行后,CPU 占用率可能瞬间飙升到 100%。 这是因为网络通道在绝大多数时间里都是"可写"的(只要底层的发送缓冲区没满)。如果一直关注 OP_WRITE,selector.select() 方法就会不断地立即返回,告诉你"可以写了",从而导致while 死循环疯狂空转(见上一篇博文中水平触发和边缘触发章节)
应对策略:
虽然是 LT 模式,但要做到"高性能不空转",不去无意义的消耗CPU
- 读数据:读完就扔给线程池。
- 写数据 :只有当 Worker 线程真正算出了结果,我们才通过
key.interestOps(ops | SelectionKey.OP_WRITE)把写事件动态添加上去。 - 一旦把缓冲区里的数据全发完了,我们立刻剔除
OP_WRITE事件。这就解决了我们在上一篇博文中 C 语言中讨论过的LT 模式下一直有额外的系统调用开销的问题。
interestOps动态增加/取消事件
动态增加一个关注事件
java
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
动态取消关注某个事件(使用按位与 & 和按位取反 ~)
java
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
实现
java
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class NioMasterSlaveReactor {
private static final int PORT = 8080;
private static final int SUB_REACTOR_COUNT = 2; // IO 搬运工数量
private static final int WORKER_THREAD_COUNT = 4; // 业务算力工人数量
// 业务线程池
private static final ExecutorService workerPool = Executors.newFixedThreadPool(WORKER_THREAD_COUNT);
public static void main(String[] args) throws IOException {
log.info(">>> Java NIO 终极架构启动 <<<");
log.info("PORT: " + PORT + " | Main(1) + Sub(" + SUB_REACTOR_COUNT + ") + Worker(" + WORKER_THREAD_COUNT + ")");
// 1. 初始化并启动 Sub-Reactors
SubReactor[] subReactors = new SubReactor[SUB_REACTOR_COUNT];
for (int i = 0; i < SUB_REACTOR_COUNT; i++) {
subReactors[i] = new SubReactor(i);
new Thread(subReactors[i], "Sub-Reactor-" + i).start();
}
// 2. 初始化 Main Reactor (单线程)
Selector mainSelector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.register(mainSelector, SelectionKey.OP_ACCEPT);
int nextReactor = 0;
// 3. Main Reactor 主循环 (专职 Accept)
while (!Thread.currentThread().isInterrupted()) {
mainSelector.select(); // 阻塞等待新连接
Iterator<SelectionKey> iterator = mainSelector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false);
// 轮询分发给 Sub-Reactor
int target = nextReactor % SUB_REACTOR_COUNT;
nextReactor++;
SubReactor reactor = subReactors[target];
// 将新连接丢入目标 Sub-Reactor 的无锁队列
reactor.newConnectionsQueue.offer(clientChannel);
// 核心:唤醒 Sub-Reactor 的 Selector (相当于 C 里的 write eventfd)
reactor.selector.wakeup();
log.info("[Main-Reactor] 接收连接,分配给 Sub-Reactor-" + target);
}
}
}
}
}
// --- 连接上下文 ---
static class Connection {
SocketChannel channel;
SubReactor subReactor;
SelectionKey key;
// 使用并发队列存放待发送的数据包,避免使用显式锁
Queue<ByteBuffer> outQueue = new ConcurrentLinkedQueue<>();
public Connection(SocketChannel channel, SubReactor subReactor) {
this.channel = channel;
this.subReactor = subReactor;
}
}
// --- Sub-Reactor (专职 IO 读写) ---
static class SubReactor implements Runnable {
private final int id;
public final Selector selector;
// 存放 Main Reactor 丢过来的新连接
public final Queue<SocketChannel> newConnectionsQueue = new ConcurrentLinkedQueue<>();
public SubReactor(int id) throws IOException {
this.id = id;
this.selector = Selector.open();
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 阻塞等待,直到有网络事件,或者被主线程 wakeup() 唤醒
selector.select();
// 1. 处理主线程扔过来的新连接
handleNewConnections();
// 2. 处理已连接客户端的 IO 事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (!key.isValid()) continue;
if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleNewConnections() throws IOException {
SocketChannel client;
while ((client = newConnectionsQueue.poll()) != null) {
Connection conn = new Connection(client, this);
// 注册到自己的 Selector 上,监听可读事件,并将 conn 作为附件挂载
SelectionKey key = client.register(selector, SelectionKey.OP_READ, conn);
conn.key = key; // 保存 key 的引用,方便后续修改事件
}
}
private void handleRead(SelectionKey key) {
Connection conn = (Connection) key.attachment();
SocketChannel channel = conn.channel;
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int count = channel.read(buffer);
if (count > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String requestStr = new String(data).trim();
// 读出数据后,将其包装成任务,提交给业务线程池
workerPool.submit(() -> processBusinessLogic(conn, requestStr));
} else if (count < 0) {
// 客户端断开连接
log.info("[Sub-Reactor-" + id + "] 客户端断开连接");
key.cancel();
channel.close();
}
} catch (IOException e) {
key.cancel();
try { channel.close(); } catch (IOException ex) {}
}
}
private void handleWrite(SelectionKey key) throws IOException {
Connection conn = (Connection) key.attachment();
SocketChannel channel = conn.channel;
ByteBuffer buffer = conn.outQueue.peek();
if (buffer != null) {
channel.write(buffer); // 非阻塞写
// 如果这个包写完了,就从队列里移除
if (!buffer.hasRemaining()) {
conn.outQueue.poll();
}
}
// 如果队列空了,说明数据全发完了,取消 OP_WRITE 监听,防止死循环
if (conn.outQueue.isEmpty()) {
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
// --- 业务逻辑处理 (运行在 Worker 线程池中) ---
private void processBusinessLogic(Connection conn, String requestData) {
try {
// 模拟业务耗时
Thread.sleep(100);
// 准备响应结果
String responseStr = "[Worker Processed] " + requestData + "\n";
ByteBuffer responseBuffer = ByteBuffer.wrap(responseStr.getBytes());
// 1. 将结果放入该连接的出站队列
conn.outQueue.offer(responseBuffer);
// 2. 跨线程安全地修改 Selector 监听事件:增加 OP_WRITE
conn.key.interestOps(conn.key.interestOps() | SelectionKey.OP_WRITE);
// 3. 敲响 Sub-Reactor 的铃铛,提醒它去发数据
conn.subReactor.selector.wakeup();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
其他框架使用NIO的示例
Apache HttpClient4
Elasticsearch Java Client 8.6版本底层通信使用的是的Apache HttpClient4
-
所有请求最终都会走到org.elasticsearch.client.RestClient#performRequest,其内部会调用Apache HttpClient4的execute方法
-
execute方法会继续调用org.apache.http.impl.nio.client.AbstractClientExchangeHandler#requestConnection方法,其内部继续调用org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager#requestConnection方法,其内部继续调用连接池的lease方法,最终内部会调用processPendingRequest方法
-
其内部会调用DefaultConnectingIOReactor#connect方法发起连接请求,创建SessionRequestImpl对象并添加到requestQueue队列中并使用wakeup唤醒对应的selector

-
上述requestQueue队列和selector在另外的方法processEvents是如何工作的呢?

可以看到其实现方式和我们上述示例代码思路基本相同
-
其他一般读写数据的IO方法在BaseIOReactor中进行拓展,如下图

Kafka Java Client
- 在kafka java client中,以生产者为例,当发送消息时,最终会调用org.apache.kafka.clients.producer.internals.Sender#wakeup方法,其内部最终会调用org.apache.kafka.common.network.Selector#wakeup,该方法会调用Java NIO Selector的wakeup方法
- 底层的selector相关方法会在org.apache.kafka.clients.NetworkClient#poll中被调用,而NetworkClient#poll又在Sender中被调用

- 具体后续如何读写数据的这些细节,有兴趣的同学可自行阅读其源码
结语
至此,Linux中的select, epoll, 甚至更底层的文件描述符的行为,Reactor模式以及Multi-Reactor模式还有Java中NIO的使用例子,我们就有了一个基本的,全面的认识了