第二天:深入理解 Selector:单线程高效管理多个 Channel

在 Java 网络编程中,如果使用传统的 BIO(阻塞 I/O)模型,我们需要为每一个客户端连接创建一个线程。当并发量很大时,系统开销会变得难以承受。

Java NIO 引入了 Selector(选择器) ,这是实现 I/O 多路复用 的关键组件。它允许单线程管理多个 Channel,只有当 Channel 真正有事件(如读、写、连接)发生时,线程才进行处理。

接下来带你通过实操代码,一步步掌握 Selector 的核心用法。

核心概念:三剑客

在开始实操前,先明确三个核心角色的关系:

  1. Channel (通道):数据的传输管道(类似于 BIO 中的 Socket)。

  2. Selector (选择器):监控者,负责轮询和管理注册在它上面的 Channel。

  3. SelectionKey (选择键)Channel 和 Selector 的关联凭证。当 Channel 注册到 Selector 上时,会返回一个 SelectionKey,它包含了该 Channel 要感知的的事件以及当前的就绪状态。


实操指南:手写一个 NIO Server

我们将按照你提供的思路,分步构建代码。

1. 初始化 Selector 和 ServerSocketChannel

首先,我们需要开启一个 Selector,并创建一个服务端通道。

2. 建立联系 (Register)

将 Channel 注册到 Selector 上。这一步会返回一个 SelectionKey,代表了这个特定的注册关系。

复制代码
    // 3. 建立 Selector 和 Channel 的联系
// register(Selector sel, int ops, Object att)
// 初始状态下,服务端只关注 "客户端连接请求" (OP_ACCEPT)
SelectionKey registerKey = serverSocketChannel.register(selector, 0, null);

// 声明关注的事件
registerKey.interestOps(SelectionKey.OP_ACCEPT);

// *注:通常可以直接写成 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

Selector关联的事件非阻塞状态

3. 事件循环与处理

复制代码
    while (true) {
    // 4. select() 阻塞等待
    // 如果没有事件发生,线程会在此阻塞,不消耗 CPU
    // 一旦有关注的事件发生,方法返回
    int selectCount = selector.select();
    if (selectCount == 0) continue;

    // 5. 获取发生事件的 SelectionKey 集合
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectedKeys.iterator();

    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();

        // 6. 处理事件
        if (key.isAcceptable()) {
            handleAccept(key);
        } else if (key.isReadable()) {
            handleRead(key);
        } else if (key.isWritable()) {
            // handleWrite(key);
        } else if (key.isConnectable()) {
            // handleConnect(key);
        }

        // 7. !至关重要!:手动从集合中删除已处理的 Key
        // Selector 不会自动从 selectedKeys 中移除已处理的 Key
        // 如果不删除,下次循环时它还在,会导致空指针异常或重复处理
        iterator.remove();
    }
}

SelectionKey和SelectedKeys的关系


重点解析

1. 为什么必须手动 iterator.remove()?

  • selector.select() 仅仅是把就绪的 key 放到 selectedKeys 集合中。

  • 当你通过 key.channel().accept() 或 read() 处理了事件后,Selector 不会自动帮你清理这个集合。

  • 如果你不移除,下一次 while 循环时,这个 key 还在集合里。此时你再调用 accept() 可能返回 null(因为连接已经处理过了),或者抛出异常。

  • 总结:事件要么处理,要么取消 (key.cancel()),处理完必须从集合移除。

2. SelectionKey 的 4 种事件类型

SelectionKey 定义了 4 个常量来代表不同的事件:

|----------------|------|---------------------------------|
| 事件常量 | 说明 | 触发场景 |
| OP_ACCEPT | 接收连接 | ServerSocketChannel 准备好接收新连接时。 |
| OP_CONNECT | 连接建立 | 客户端 SocketChannel 完成与服务器的握手连接时。 |
| OP_READ | 可读 | Channel 中有数据到达,准备好被读取时。 |
| OP_WRITE | 可写 | Channel 的发送缓冲区有空闲空间,准备好写入数据时。 |

3. SelectionKey 的作用

SelectionKey 是 Selector 和 Channel 之间的桥梁。它不仅保存了关注的事件(Interest Set),还保存了已经发生的事件(Ready Set)。

  • 通过 key.channel() 可以获取关联的 Channel。

  • 通过 key.selector() 可以获取关联的 Selector。

  • 通过 key.attach(Object obj) 还可以绑定额外的对象(如 Buffer),实现更复杂的逻辑。


总结

使用 Selector 管理多个 Channel 的核心流程归纳为:Open -> Register -> Select -> Handle -> Remove

相关推荐
JAVA面经实录9176 小时前
Java企业级工程化·终极完整版背诵手册(无遗漏、全覆盖、面试+落地通用)
java·开发语言·面试
许彰午8 小时前
CacheSQL(二):主从复制——OpLog 环形缓冲区与故障自动恢复
java·数据库·缓存
Bat U9 小时前
JavaEE|多线程初阶(七)
java·开发语言
掌心向暖RPA自动化11 小时前
如何获取网页某个元素在屏幕可见部分的中心坐标影刀RPA懒加载坐标定位技巧
java·javascript·自动化·rpa·影刀rpa
日取其半万世不竭11 小时前
Minecraft Java版社区服务器搭建教程(Linux,适合新手)
java·linux·服务器
TeamDev12 小时前
JxBrowser 9.0.0 版本发布啦!
java·前端·混合应用·jxbrowser·浏览器控件·跨平台渲染·原声输入
AI人工智能+电脑小能手12 小时前
【大白话说Java面试题】【Java基础篇】第24题:Java面向对象有哪些特征
java·开发语言·后端·面试
AI人工智能+电脑小能手13 小时前
【大白话说Java面试题】【Java基础篇】第25题:JDK1.8的新特性有哪些
java·开发语言·后端·面试
likerhood13 小时前
SLF4J: Failed to load class “StaticLoggerBinder“ 解决
java·log4j·maven
早日退休!!!13 小时前
大模型推理瓶颈七层分析模型
java·服务器·数据库