0
0
0

Java NIO:Selector 选择器与 IO 多路复用

2026-11-02
2026-11-02
文章摘要
|

一、什么是 Selector?

Selector(选择器)是 Java NIO 的核心组件之一,主要用于监听多个 Channel 的 IO 就绪事件。

传统 BIO 通常采用一个客户端连接对应一个处理线程的方式。当客户端数量较多时,大量线程可能处于阻塞等待状态,增加内存占用和线程切换成本。

Java NIO 可以通过 Selector 实现 IO 多路复用,使一个线程管理多个客户端连接。

例如,服务器同时存在 3 个客户端:

                  IO线程
                    |
                    v
                 Selector
                    |
          +---------+---------+
          |         |         |
          v         v         v
       Channel A Channel B Channel C
          |         |         |
         可读     暂无数据     可读

Selector 可以检测到 Channel A、Channel C 有可读事件,线程再分别处理对应的数据。

核心理解:

  1. Selector 负责监听多个 Channel。

  2. Channel 负责实际的数据传输。

  3. Buffer 负责存储读取和写入的数据。

  4. 一个 Selector 可以注册多个 Channel。

  5. 一个线程可以通过 Selector 管理多个客户端连接。

注意:一个线程管理多个连接,不代表这些连接的业务逻辑会自动并行执行。

二、Selector 的工作原理

Selector 的核心机制是 IO 多路复用(I/O Multiplexing)。

IO 多路复用指的是:一个线程可以同时等待多个 IO 通道的就绪事件,而不需要为每个连接单独创建阻塞等待线程。

基本流程:

创建 Selector
      |
      v
创建 ServerSocketChannel
      |
      v
设置非阻塞模式
      |
      v
注册 OP_ACCEPT 事件
      |
      v
selector.select()
      |
      | 等待 IO 就绪事件
      v
获取 SelectionKey
      |
      +---- OP_ACCEPT ----> 接收客户端连接
      |                          |
      |                          v
      |                    注册 OP_READ
      |
      +---- OP_READ ------> 读取客户端数据
      |
      +---- OP_WRITE -----> 发送待写入数据
      |
      +---- OP_CONNECT ---> 完成非阻塞连接
      |
      v
继续下一轮监听

底层通常由操作系统提供的 IO 多路复用机制支持,例如 Linux 下的 epoll、其他平台上的相应机制。

Selector 负责监听 IO 是否就绪,真正的数据读写仍然需要通过 Channel 完成。

三、Selector 核心 API

1. Selector.open()

创建选择器:

Selector selector = Selector.open();

用于监听多个 Channel 的 IO 事件。

2. configureBlocking(false)

注册 Selector 前,Channel 必须设置为非阻塞模式。

ServerSocketChannel server =
        ServerSocketChannel.open();

server.configureBlocking(false);

如果 Channel 仍然处于阻塞模式,注册 Selector 时会抛出:

IllegalBlockingModeException

注意:这里设置的是 Channel 的阻塞模式,并不是 Selector 自身的阻塞模式。

3. register()

将 Channel 注册到 Selector。

server.register(
        selector,
        SelectionKey.OP_ACCEPT
);

含义:

将 ServerSocketChannel 注册到 Selector,关注客户端连接就绪事件。

也可以获取注册后返回的 SelectionKey:

SelectionKey key = server.register(
        selector,
        SelectionKey.OP_ACCEPT
);

SelectionKey 代表 Channel 在指定 Selector 上的注册关系。

它保存的信息主要包括:

  • 注册的 Channel。

  • 所属 Selector。

  • 关注的事件集合(interestOps)。

  • 已经就绪的事件集合(readyOps)。

  • 可以附加的业务对象(attachment)。

4. select()

selector.select();

等待已注册 Channel 出现 IO 就绪事件。

常见方法:

方法

作用

select()

可以阻塞等待 IO 事件

select(1000)

最多等待约 1000 毫秒

selectNow()

立即检查,不阻塞等待

wakeup()

唤醒当前或下一次选择操作

为什么 NIO 是非阻塞 IO,select() 却会阻塞?

因为两者的作用不同。

channel.configureBlocking(false);

表示 Channel 采用非阻塞 IO 模式。

selector.select();

表示当前线程可以阻塞等待 IO 就绪事件,避免不断轮询导致 CPU 空转。

因此,NIO 的非阻塞 Channel 与 Selector 的阻塞等待并不矛盾。

注意:select() 返回不保证一定有可读数据,可能因为其他事件或 wakeup() 而返回。

5. selectedKeys()

获取已经就绪的 SelectionKey 集合。

Set<SelectionKey> keys =
        selector.selectedKeys();

通常使用迭代器进行遍历:

Iterator<SelectionKey> iterator =
        selector.selectedKeys().iterator();

while (iterator.hasNext()) {

    SelectionKey key = iterator.next();

    // 移除已处理的就绪记录
    iterator.remove();

    if (!key.isValid()) {
        continue;
    }

    if (key.isAcceptable()) {
        // 接收客户端连接
    }

    if (key.isReadable()) {
        // 读取数据
    }
}

这里必须理解:

iterator.remove() 是将当前 Key 从 selectedKeys 集合中移除。

它不会关闭 Channel,也不会取消 Channel 的注册。

四、SelectionKey 的四种事件

事件

含义

适用场景

OP_ACCEPT

连接可以被接收

ServerSocketChannel

OP_CONNECT

非阻塞连接操作就绪

SocketChannel

OP_READ

Channel 可读

SocketChannel

OP_WRITE

Channel 可写

SocketChannel

1. OP_ACCEPT:接收客户端连接

server.register(
        selector,
        SelectionKey.OP_ACCEPT
);

当有客户端连接就绪:

if (key.isAcceptable()) {

    ServerSocketChannel serverChannel =
            (ServerSocketChannel) key.channel();

    SocketChannel client = serverChannel.accept();

    if (client != null) {

        // 新客户端也需要设置非阻塞
        client.configureBlocking(false);

        // 注册可读事件
        client.register(
                selector,
                SelectionKey.OP_READ
        );
    }
}

注意:

ServerSocketChannel 负责监听客户端连接。

accept() 成功后返回 SocketChannel,后续由 SocketChannel 负责数据读写。

ServerSocketChannel 设置为非阻塞,并不会让新接收的 SocketChannel 自动变成非阻塞模式,需要单独设置。

2. OP_READ:读取数据

client.register(
        selector,
        SelectionKey.OP_READ
);

当 Channel 有可读事件:

if (key.isReadable()) {

    SocketChannel client =
            (SocketChannel) key.channel();

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    int length = client.read(buffer);

    if (length > 0) {

        buffer.flip();

        // 处理数据

    } else if (length == -1) {

        key.cancel();
        client.close();
    }
}

read() 返回值:

返回值

含义

大于 0

本次成功读取的字节数

0

本次没有读取到数据

-1

对端输出流结束

注意:OP_READ 就绪只代表可以执行读取操作,不代表一定收到了一条完整的业务消息。

TCP 是字节流协议,仍然需要解决拆包和粘包问题。

3. OP_WRITE:写入数据

OP_WRITE 表示 Channel 当前可以进行写入操作。

但是需要特别注意:

不要一直注册 OP_WRITE 事件。

因为 SocketChannel 在发送缓冲区有空间时,通常就处于可写状态。

即使业务没有数据需要发送,OP_WRITE 也可能持续就绪。

结果就是:

Selector 检测到 OP_WRITE
        |
        v
处理可写事件
        |
        v
再次调用 select()
        |
        v
再次检测到 OP_WRITE
        |
        v
不断重复,CPU 占用过高

推荐方式:

当存在待发送数据,并且非阻塞写入没有完成时,再关注 OP_WRITE。

// 开启可写事件
key.interestOps(
        key.interestOps() | SelectionKey.OP_WRITE
);

当待发送数据全部写完:

// 取消对可写事件的关注
key.interestOps(
        key.interestOps() & ~SelectionKey.OP_WRITE
);

注意:取消 OP_WRITE 并不是关闭 SocketChannel,只是不再关注可写事件。

如果其他线程修改 interestOps,并且希望阻塞中的 Selector 及时响应,还需要根据线程模型适当调用 selector.wakeup()。

4. OP_CONNECT:连接就绪

主要用于非阻塞 SocketChannel 主动连接服务端。

例如:

SocketChannel client = SocketChannel.open();

client.configureBlocking(false);

boolean connected = client.connect(
        new InetSocketAddress("127.0.0.1", 8080)
);

如果连接没有立即完成,可以注册 OP_CONNECT。

if (!connected) {
    client.register(
            selector,
            SelectionKey.OP_CONNECT
    );
}

连接事件就绪后:

if (key.isConnectable()) {

    SocketChannel channel =
            (SocketChannel) key.channel();

    if (channel.finishConnect()) {

        // 连接完成,改为关注可读事件
        key.interestOps(SelectionKey.OP_READ);
    }
}

注意:

OP_CONNECT 不是服务端接收连接的事件。

  • OP_ACCEPT:服务端接收客户端连接。

  • OP_CONNECT:客户端完成非阻塞连接过程。

五、Java 8 完整代码示例

以下实现一个简单的 NIO TCP 服务端。

功能:

  1. 使用 Selector 监听多个客户端。

  2. 接收客户端连接。

  3. 读取客户端数据。

  4. 处理连接断开。

  5. 不需要为每个客户端单独创建线程。

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;

public class NioSelectorServer {

    public static void main(String[] args) throws Exception {

        try (Selector selector = Selector.open();
             ServerSocketChannel server =
                     ServerSocketChannel.open()) {

            // 1. 设置非阻塞模式
            server.configureBlocking(false);

            // 2. 绑定端口
            server.bind(new InetSocketAddress(8080));

            // 3. 注册连接事件
            server.register(
                    selector,
                    SelectionKey.OP_ACCEPT
            );

            System.out.println("NIO 服务端已启动");

            while (true) {

                // 4. 等待 IO 事件就绪
                selector.select();

                Iterator<SelectionKey> iterator =
                        selector.selectedKeys().iterator();

                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();

                    // 5. 移除已处理的就绪记录
                    iterator.remove();

                    if (!key.isValid()) {
                        continue;
                    }

                    try {

                        // 6. 新客户端连接
                        if (key.isAcceptable()) {

                            ServerSocketChannel serverChannel =
                                    (ServerSocketChannel) key.channel();

                            SocketChannel client;

                            while ((client = serverChannel.accept()) != null) {

                                client.configureBlocking(false);

                                // 为每个客户端注册读取事件
                                client.register(
                                        selector,
                                        SelectionKey.OP_READ
                                );

                                System.out.println(
                                        "客户端连接:" +
                                                client.getRemoteAddress()
                                );
                            }
                        }

                        // 7. 客户端发送数据
                        if (key.isReadable()) {

                            SocketChannel client =
                                    (SocketChannel) key.channel();

                            ByteBuffer buffer =
                                    ByteBuffer.allocate(1024);

                            int length = client.read(buffer);

                            if (length > 0) {

                                buffer.flip();

                                // 此处仅演示字节接收,不处理业务消息边界
                                System.out.println(
                                        "本次收到:" +
                                                buffer.remaining() +
                                                " 字节"
                                );

                            } else if (length == -1) {

                                key.cancel();
                                client.close();

                                System.out.println("客户端断开");
                            }
                        }

                    } catch (IOException e) {

                        // 单个连接异常时,清理对应 Channel
                        key.cancel();

                        try {
                            key.channel().close();
                        } catch (IOException ignored) {
                            // 避免关闭异常影响事件循环
                        }

                        System.out.println(
                                "连接异常:" + e.getMessage()
                        );
                    }
                }
            }
        }
    }
}

这个示例是为了说明 Selector 的事件处理流程。

实际生产中还需要进一步完善:

  • 每个连接的接收缓冲区管理。

  • TCP 拆包和粘包。

  • 待发送消息队列。

  • 非阻塞部分写入和 OP_WRITE 管理。

  • 业务线程池与异常处理。

  • 心跳检测和连接超时清理。

例如,TCP 一条消息只读取了一部分,不能直接丢弃剩余数据,需要根据业务协议保留并等待下一次读取。

六、Selector 常见问题与踩坑

1. 没有将 Channel 设置为非阻塞模式

错误:

ServerSocketChannel server =
        ServerSocketChannel.open();

server.register(
        selector,
        SelectionKey.OP_ACCEPT
);

原因:

ServerSocketChannel 默认是阻塞模式,不能直接注册到 Selector。

正确:

server.configureBlocking(false);

server.register(
        selector,
        SelectionKey.OP_ACCEPT
);

2. selectedKeys() 遍历后没有 remove()

错误:

while (iterator.hasNext()) {

    SelectionKey key = iterator.next();

    // 处理事件,但没有移除就绪记录
}

这样可能导致下一轮继续处理旧的就绪记录。

正确:

while (iterator.hasNext()) {

    SelectionKey key = iterator.next();

    iterator.remove();

    // 处理事件
}

需要区分:

方法

作用

iterator.remove()

从 selectedKeys 中移除就绪记录

key.cancel()

取消 Channel 在 Selector 上的注册

channel.close()

关闭 Channel

3. 长期关注 OP_WRITE

错误:

client.register(
        selector,
        SelectionKey.OP_READ | SelectionKey.OP_WRITE
);

如果没有需要发送的数据,长期监听 OP_WRITE 可能导致大量无意义的可写事件。

推荐:默认主要关注 OP_READ,有待发送数据且需要等待通道可写时才关注 OP_WRITE。

4. 误以为 select() 返回就一定有数据

错误理解:

selector.select();

// 认为客户端一定发送了数据

实际上 select() 返回可能因为:

  • 客户端连接就绪。

  • Channel 可读。

  • Channel 可写。

  • 非阻塞连接就绪。

  • wakeup() 被调用。

  • 线程中断等其他原因。

需要根据 SelectionKey 判断事件类型,而不能默认就是可读事件。

5. 在 Selector 线程中执行耗时操作

错误示例:

if (key.isReadable()) {

    // 模拟耗时业务
    Thread.sleep(5000);
}

如果只有一个 Selector 线程,那么它在执行耗时任务期间,其他客户端的 IO 事件就不能得到及时处理。

例如:

Channel A:执行耗时任务 5 秒
Channel B:等待处理
Channel C:等待处理

推荐方式:

IO 线程主要负责网络数据接收、发送和必要的轻量处理。

耗时数据库操作、远程 HTTP 请求以及其他阻塞业务,可以根据业务需求交给独立线程池。

不过跨线程处理时也需要注意并发安全、消息顺序和背压控制。

6. 误以为 Selector 能解决 TCP 拆包和粘包

Selector 只能告诉程序 Channel 当前是否有数据可读。

例如客户端发送:

消息A:100字节
消息B:200字节

服务端可能分多次接收,也可能一次接收到包含多条业务消息的字节流。

Selector 不负责识别业务消息边界。

需要使用固定长度、分隔符或长度字段等协议规则完成消息解析。

七、Selector 和 Netty EventLoop 的关系

Java NIO 提供:

Selector
Channel
Buffer
SelectionKey

Netty 在 NIO 等底层能力基础上,提供:

NioEventLoop
Channel
ByteBuf
ChannelPipeline
ChannelHandler

两者存在密切关系。

例如 Netty 的 NioEventLoop 会负责:

  1. 管理和监听 Channel。

  2. 通过 Selector 等待 IO 事件。

  3. 处理网络 IO。

  4. 执行提交到 EventLoop 的任务。

  5. 配合 Netty 的 Pipeline 传播和处理 IO 事件。

可以简单理解为:

Selector 是 Java NIO 的事件监听组件,而 Netty 的 NioEventLoop 是基于事件循环机制封装的 IO 线程执行单元。

Netty 不仅封装了 Selector,还提供了编解码、任务调度、内存管理和连接管理等能力。

八、重点总结

  1. Selector 是 Java NIO 的核心组件,用于监听多个 Channel 的 IO 就绪事件。

  2. IO 多路复用使一个线程能够管理多个客户端连接。

  3. 注册 Selector 的 Channel 必须使用非阻塞模式。

  4. register() 用于注册 Channel 和关注的 IO 事件。

  5. SelectionKey 代表 Channel 在 Selector 上的注册关系。

  6. select() 可以阻塞等待事件,但 Channel 仍然可以采用非阻塞 IO。

  7. OP_ACCEPT:接收客户端连接。

  8. OP_CONNECT:完成非阻塞客户端连接。

  9. OP_READ:监听可读事件。

  10. OP_WRITE:监听可写事件,应避免无意义地长期关注。

  11. selectedKeys() 用于获取已就绪的 Key。

  12. iterator.remove() 只移除就绪记录,不会关闭连接。

  13. key.cancel() 取消注册,但不能代替 channel.close() 释放连接资源。

  14. Selector 不负责 TCP 拆包和粘包。

  15. Selector 的 IO 线程应避免执行耗时阻塞任务。

九、常见问题与答案

1. 为什么一个 Selector 能监听多个 Channel?

因为 Selector 利用操作系统的 IO 多路复用能力,统一等待多个 Channel 的 IO 就绪事件,不需要让每个连接分别占用一个阻塞线程。

2. NIO 是非阻塞 IO,为什么 select() 会阻塞?

因为 select() 是等待 IO 事件就绪,而非阻塞 Channel 的 read()、write() 等操作不会因为等待数据或空间而无限阻塞。两者并不矛盾。

3. SelectionKey 有什么作用?

记录 Channel 在 Selector 上的注册关系、关注的事件和已经就绪的事件,也支持关联业务对象。

4. 为什么不能一直注册 OP_WRITE?

因为 SocketChannel 通常很容易处于可写状态,长期监听可能导致 Selector 持续返回可写事件,造成 CPU 空转。

5. 为什么需要 iterator.remove()?

因为 selectedKeys() 中已处理的 Key 不会自动移除,需要手动清理,避免下一轮重复处理旧的就绪记录。

6. key.cancel() 和 iterator.remove() 有什么区别?

iterator.remove() 只是移除已选出的就绪记录;key.cancel() 则取消 Channel 在 Selector 上的注册。

7. Selector 能不能保证读取到完整的 TCP 消息?

不能。Selector 只负责 IO 事件就绪通知。业务消息是否完整,需要通过协议解析判断。

8. Selector 和 Netty 的 EventLoop 有什么区别?

Selector 主要负责 IO 事件监听;Netty EventLoop 则负责执行事件循环、处理 IO 事件和相关任务,功能更完善。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论