← 返回题目列表

Java NIO 中的 Channel、Buffer 和 Selector 分别有什么作用?

高频 中等 第 10 / 22 题 更新于 2026/07/25
NIOChannelBufferSelector

简化版

Channel 是连接文件、Socket 等 I/O 资源的通道,Buffer 是数据读写的内存容器,Selector 是可选择通道的多路复用器。程序从 Channel 读数据到 Buffer,或从 Buffer 写入 Channel;一个 Selector 可以监听多个非阻塞 Channel 的就绪事件。

详细版

NIO 的三个核心角色是:

  • Channel:表示 I/O 连接。FileChannelSocketChannelServerSocketChannel 分别面向文件、TCP 客户端和 TCP 服务端。Channel 可以读、写或同时支持两者,具体能力由通道类型和打开方式决定。
  • Buffer:装载数据。最常用的是 ByteBuffer,通过 positionlimitcapacity 描述当前可读写区间。
  • Selector:监听多个 SelectableChannel 是否出现连接、接收、读、写就绪事件,让一个事件循环管理多个连接。

Selector 只告诉程序“哪些操作现在可以尝试”,不替程序读写数据,也不保证一次就能把数据全部读完或写完。

完整版教学

一、Channel 与传统 Stream 的差异

传统流通常以连续字节序列的方式读写;Channel 的常见 read / write 操作通过 Buffer 传递数据。Channel 体系还支持非阻塞网络 I/O、分散读取、聚集写入、文件映射和通道间传输等能力;其中映射和 transferTo 等特定操作不需要应用自己提供中间 Buffer。

不要把“Channel”简化成“一定双向”。SocketChannel 可读可写,但 ServerSocketChannel 主要接受连接,而一个只按 READ 方式打开的 FileChannel 也不能写。

二、Buffer 是数据状态机

刚创建的 ByteBuffer 通常处于写模式:position = 0limit = capacity。Channel 将数据写入 Buffer 后,需要调用 flip() 把它切换到读模式;处理完后用 clear() 重用整个缓冲区,或用 compact() 保留没读完的数据。

Buffer 的状态是 NIO 代码的高频 bug 来源。忘记 flip() 可能导致读不到数据,不处理半包就 clear() 则可能丢数据。

三、Selector 怎么串起多个连接

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

    server.configureBlocking(false);
    server.bind(new InetSocketAddress(8080));
    server.register(selector, SelectionKey.OP_ACCEPT);

    while (!Thread.currentThread().isInterrupted()) {
        selector.select();
        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

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

            if (!key.isValid()) continue;
            if (key.isAcceptable()) {
                SocketChannel client = server.accept();
                if (client != null) {
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ,
                            ByteBuffer.allocate(4096));
                }
            }
        }
    }
}

通道必须先切换为非阻塞模式才能注册到 Selector。注册结果是 SelectionKey,它记录通道、Selector、关心的事件和已就绪事件。可以通过 attachment 绑定每个连接的 Buffer 或业务上下文。

四、实战中的几个坑

  • 处理完 selectedKeys() 中的 key 后要从集合中移除,否则下一轮可能重复处理旧事件。
  • 读就绪不等于一条完整业务消息已经到达,TCP 可能出现拆包和粘包,应该由协议的长度字段、分隔符或固定长度解决边界。
  • write() 可能只写出 Buffer 的一部分。剩余数据要保留,并注册 OP_WRITE 等待下次可写;没有待写数据时不应长期关心 OP_WRITE,否则容易造成空转。
  • FileChannel 不是 SelectableChannel,普通磁盘文件不能像 Socket 一样注册到 Selector。

五、用一次半包和部分写串起三者

假设每条消息由 4 字节长度头和 100 字节正文组成,而第一次可读事件只收到 60 字节。Channel 把 60 字节放入 Buffer;解码器读出长度后发现正文还差 44 字节,必须 compact() 保留未完成帧。下一次 OP_READ 到达后再拼接,不能把第一次 read 当作完整消息。

响应共 104 字节时,一次非阻塞 write 可能只写出 40 字节,Buffer 的 position 会推进 40。剩余 64 字节应保存在连接上下文并临时关注 OP_WRITE,写完后取消写兴趣,避免可写事件空转。

Selector 找就绪连接 → Channel 搬运字节 → Buffer 保留游标与半包状态
                    ↑                     ↓
                 SelectionKey attachment 保存每连接上下文
SelectionKey 集合含义常见操作
interest set应用关心哪些操作interestOps(...)
ready set本轮哪些操作已就绪readyOps() / isReadable
selected-key set待应用处理的 key迭代后移除

六、并发修改、wakeup 与背压

Selector 常由单一事件线程拥有,以避免多个线程同时修改连接状态。业务线程若产生响应,可先放入线程安全队列,再调用 wakeup() 让阻塞的选择操作及时返回,由事件线程统一修改 interest set;wakeup 是通知机制,不替代队列和内存可见性设计。

若写端持续慢于生产端,待发送 Buffer 会不断累积。假设每个连接积压 2 MiB、共有 500 个慢连接,待写数据就约为 1,000 MiB,因此必须设置高低水位、暂停读取或拒绝请求,而不是无界注册 OP_WRITE。

记忆钩子:Selector 管“谁能做”,Channel 管“和谁搬”,Buffer 管“搬到哪里以及搬到哪一步”;attachment 管“这个连接下一步做什么”。

七、常见误区与追问

  • 误区:所有 Channel 都能注册到 Selector。 只有 SelectableChannel 可以,普通 FileChannel 不属于这一体系。
  • 误区:就绪表示操作一定一次完成。 就绪只表示可以尝试,read/write 都可能部分完成,非阻塞调用也可能返回 0。
  • 误区:处理 selected key 后不需要移除。 传统 selected-key set 不会由 Selector 自动清空,不移除会重复处理旧 key。
  • 误区:OP_WRITE 应从连接建立起永久注册。 大多数 Socket 经常可写,这会造成事件循环空转,应只在确有积压时关注。
  • 追问:SelectionKey attachment 有什么用? 它把 Buffer、解码器、待写队列等每连接状态与注册关系绑定起来。
  • 追问:跨线程修改兴趣集为什么常配合 wakeup? 让阻塞在 select 的事件线程及时返回并观察队列中的变更,降低响应延迟。
  • 追问:Selector 如何处理业务背压? Selector 本身不提供完整策略,应用需限制队列、切换读兴趣、设置水位或关闭过慢连接。

八、加强记忆

按数据链理解三者:Selector 找出已就绪的可选择 Channel,Channel 负责连接 I/O 资源,Buffer 承载真正读写的数据。Selector 只报告就绪事件,不会替应用读完数据;半包、部分写和 Buffer 状态仍需要应用代码处理。