Skip to content

Java NIO原理 ​

看在前面 ​

BIO 模型的问题 ​

经典 BIO 是"每连接每线程":主线程 accept() 后把连接交给线程池处理。缺点是严重依赖线程,而线程是"贵"的资源:

  • 线程的创建/销毁是重量级系统调用;
  • 线程栈默认占用 512K~1M 内存;
  • 线程切换成本高(CPU sy 超过 20% 时系统接近不可用);
  • 容易造成锯齿状的系统负载。

结论:连接数小于单机 1000 时 BIO 可用,面对十万/百万级长连接无能为力。

NIO 是怎么工作的 ​

1 I/O 的两个阶段 ​

所有 I/O 分为两步:等待就绪(如等待网卡数据到达 RecvBuffer)和数据操作(真正的读写,本质是 memory copy,很快)。等待就绪的阻塞并不占 CPU(空等)。

2 三种 I/O 模型对比 ​

以 socket.read() 为例:

  • BIO:RecvBuffer 没有数据就一直阻塞;
  • NIO:有数据就读取返回,没有数据直接返回 0,永不阻塞。等待就绪阶段非阻塞,但真正的 I/O 操作仍是同步的;
  • AIO:连数据从网卡到内核缓冲区的拷贝过程也是异步的,完成后回调通知。

一句话概括:BIO 关心"我要读",NIO 关心"我可以读了",AIO 关注"读完了"。

3 三大组件 ​

  • Channel:对应连接及事件(读、写或连接);
  • Selector:事件选择器。select() 本身是阻塞的(Linux 2.6 前基于 select/poll,之后用 epoll,Windows 是 IOCP),可以放心放在 while(true) 中调用,不会 CPU 空转;
  • Buffer:DirectByteBuffer 可以减少一次内核空间到用户空间的拷贝。但创建/销毁成本高,需要内存池配合。小数据量用 HeapByteBuffer,大数据量用 DirectByteBuffer(选型细节见库内 怎样选择HeapByteBuffer与DirectByteBuffer)。

Reactor 模式 ​

核心思想:注册"读就绪、写就绪、新连接"三类事件的处理器。单线程轮询选择就绪事件并执行对应处理器:

java
interface ChannelHandler {
    void channelReadable(Channel channel);
    void channelWritable(Channel channel);
}
class Channel {
    Socket socket;
    Event event; // 读,写或者连接
}
class IoThread extends Thread {
    public void run() {
        Channel channel;
        while (channel = Selector.select()) { // 选择就绪的事件和对应的连接
            if (channel.event == accept) registerNewChannelHandler(channel);
            if (channel.event == write)  getChannelHandler(channel).channelWritable(channel);
            if (channel.event == read)   getChannelHandler(channel).channelReadable(channel);
        }
    }
}
Map<Channel, ChannelHandler> handlerMap;

(示意伪代码,不可用于生产环境。)

1 Reactor vs Proactor ​

  • Reactor 基于同步 I/O:分发器只通知"可读写了",实际读写由用户处理器完成;
  • Proactor 基于异步 I/O(如 Windows overlapped I/O):处理器发起异步操作,操作系统完成读写后再通知"已完成";
  • 可以把 Reactor 改造成"模拟 Proactor":把读写从事件处理器移到分发器,分发器读到数据后再回调 channelReadComplete。工作量不变、性能不削弱。还能对不支持异步 API 的系统暴露统一的异步接口。

2 线程模型优化 ​

多核机器上把线程职责拆开:

  • 事件分发器:单线程选择就绪事件;
  • I/O 处理器:纯 CPU 操作,开 CPU 核心数个线程;
  • 业务线程:含 DB、RPC 这类阻塞操作时必须独立线程池;
  • 限制:一个 socket 只能属于一个 IoThread,一个 IoThread 可以管理多个 socket。

NIO 在客户端的魔力 ​

  • Redis 客户端:单线程 + 请求队列,pipeline 批量发送请求并返回 future,channel 可读时从队列取出 future 并 done();
  • HttpClient 短连接抓取:单线程做连接/读/写,以 socket 引用作为 map 的 key 保存请求/响应上下文;
  • Thrift/Dubbo 等 RPC:以请求号为 key、result 为 future 的 map,配合 NIO + 长连接实现同步语义。

Selector.wakeup() ​

作用:解除阻塞在 select()/select(long) 上的线程。两次 select 之间的多次调用等价于一次。没有线程阻塞时作用于下一次 select("记忆"作用)。触发场景:注册新 channel/事件、channel 关闭取消注册、更高优先级事件(如定时器任务)到期。

原理:Linux 用 pipe,Windows 用 loopback TCP 连接(win32 管道无法加入 fd set)。往管道写入一个字节使 select 立即返回,开销不可忽视。

存在的问题 ​

  • 使用 NIO 不等于高性能:连接数 <1000、并发不高或局域网环境下没有明显优势;
  • 没有完全屏蔽平台差异,仍基于各操作系统的 I/O 实现;
  • 事件驱动模型编程不易、陷阱重重;
  • 优化方向:使用成熟框架如 Netty、MINA。

总结 ​

NIO 的价值在于:事件驱动模型、避免多线程、单线程处理多任务、非阻塞 I/O(读写直接返回 0)。以及基于 block 的传输更高效、zero-copy、IO 多路复用带来的可伸缩性。

内容来源:原文 · 2026-10 整理回填。