彻底理解Java NIO(三)Java实现 I/O多路复用+Reactor模式及开源框架代码解读

文章目录

上一篇文章介绍了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接口

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 上:

  1. 线程 B 调用 channel.register(selector, ...)
  2. 此时,线程 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_READOP_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

  1. 读数据:读完就扔给线程池。
  2. 写数据 :只有当 Worker 线程真正算出了结果,我们才通过 key.interestOps(ops | SelectionKey.OP_WRITE) 把写事件动态添加上去。
  3. 一旦把缓冲区里的数据全发完了,我们立刻剔除 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

  1. 所有请求最终都会走到org.elasticsearch.client.RestClient#performRequest,其内部会调用Apache HttpClient4的execute方法

  2. execute方法会继续调用org.apache.http.impl.nio.client.AbstractClientExchangeHandler#requestConnection方法,其内部继续调用org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager#requestConnection方法,其内部继续调用连接池的lease方法,最终内部会调用processPendingRequest方法

  3. 其内部会调用DefaultConnectingIOReactor#connect方法发起连接请求,创建SessionRequestImpl对象并添加到requestQueue队列中并使用wakeup唤醒对应的selector

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

    可以看到其实现方式和我们上述示例代码思路基本相同

  5. 其他一般读写数据的IO方法在BaseIOReactor中进行拓展,如下图

Kafka Java Client

  1. 在kafka java client中,以生产者为例,当发送消息时,最终会调用org.apache.kafka.clients.producer.internals.Sender#wakeup方法,其内部最终会调用org.apache.kafka.common.network.Selector#wakeup,该方法会调用Java NIO Selector的wakeup方法
  2. 底层的selector相关方法会在org.apache.kafka.clients.NetworkClient#poll中被调用,而NetworkClient#poll又在Sender中被调用
  3. 具体后续如何读写数据的这些细节,有兴趣的同学可自行阅读其源码

结语

至此,Linux中的select, epoll, 甚至更底层的文件描述符的行为,Reactor模式以及Multi-Reactor模式还有Java中NIO的使用例子,我们就有了一个基本的,全面的认识了

相关推荐
程序员黑豆1 小时前
AI全栈开发 - Java:数据类型
java·前端
曹牧1 小时前
Java:Xml中的大、小于
java·开发语言
zavoryn1 小时前
Jackson 序列化踩坑:LocalDateTime、Long 精度丢失和 boolean isXxx 字段
java·开发语言·后端
江华森1 小时前
Tomcat 10 实战部署指南:从零到生产级 Web 容器
java·前端·tomcat
曹牧1 小时前
Java:XML转义
xml·java·开发语言
swordbob1 小时前
【RabbitMQ】消息丢失的 6 大场景及解决方案
后端·rabbitmq
leo_yu_yty1 小时前
Go语言分布式计算(并发Debug)
开发语言·笔记·后端·golang
心之伊始1 小时前
Dubbo 3 Consumer 调用链路源码分析:从 Proxy 到 Cluster、Directory、Router、LoadBalance
java·微服务·dubbo·源码分析·服务治理
我认不到你1 小时前
【开源、教程】RAG全流程实现(java+完整代码):第一弹
java·开发语言·人工智能·深度学习·ai·语言模型·开源