视频看了几百小时还迷糊?关注我,几分钟让你秒懂!


一、为什么需要理解 I/O 模型?

你在写 Web 接口时,是否想过:

“当用户发起一个 HTTP 请求,操作系统和 JVM 到底经历了什么?”

答案就藏在 I/O 模型 中。它是高性能服务器(如 Nginx、Redis、Netty)的底层基石。
不懂 I/O 模型,你就无法真正理解 为什么 Netty 能扛住百万并发

今天,我们从 操作系统层面 出发,结合 Java 实现,彻底讲透五大 I/O 模型!


二、I/O 的两个阶段(关键!)

任何一次网络 I/O(如 read())都分为两个阶段:

  1. 等待数据准备就绪(Waiting for data to be ready)
    • 例如:网卡收到 TCP 包,数据从内核缓冲区准备好
  2. 将数据从内核复制到用户空间(Copying the data from kernel to process)

所有 I/O 模型的区别,就在于这两个阶段是否阻塞、是否需要主动轮询、是否由系统通知!


三、五大 I/O 模型详解(附图解)

1️⃣ 阻塞 I/O(Blocking I/O)—— BIO

  • 阶段1:阻塞(直到数据就绪)
  • 阶段2:阻塞(直到复制完成)
  • 特点:全程卡住线程,啥也干不了
🖼️ 流程图:
[应用线程] 
    ↓ 调用 read()
    → [阻塞] 等待数据到达
    → [阻塞] 内核复制数据
    ← 返回数据
✅ Java 示例(传统 Socket):
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept(); // 阻塞
InputStream in = client.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf); // 阻塞!直到有数据或连接关闭

⚠️ 问题:1 万连接 → 1 万线程 → OOM!


2️⃣ 非阻塞 I/O(Non-blocking I/O)

  • 阶段1:不阻塞(立即返回,若无数据则返回错误)
  • 阶段2:阻塞(复制时仍会卡住)
  • 特点:需轮询检查数据是否就绪
🖼️ 流程图:
[应用线程]
    ↓ 调用 read()
    → 立即返回 EWOULDBLOCK(无数据)
    → sleep / do other work
    → 再次调用 read() ...
    → 直到数据就绪 → 阻塞复制 → 返回
✅ Java 示例(NIO Channel):
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 设为非阻塞

ByteBuffer buf = ByteBuffer.allocate(1024);
while (true) {
    int len = channel.read(buf); // 若无数据,立即返回 -1 或 0
    if (len > 0) {
        // 处理数据
        break;
    }
    Thread.sleep(10); // 轮询!浪费 CPU
}

❌ 缺点:忙等(busy-waiting),CPU 空转!


3️⃣ I/O 多路复用(I/O Multiplexing)—— NIO 的核心!

  • 使用 select/poll/epoll/kqueue 等系统调用
  • 阶段1:阻塞在 select() 上(但可监听多个 fd)
  • 阶段2:阻塞(复制数据)
  • 特点单线程管理多个连接,事件驱动
🖼️ 流程图:
[应用线程]
    ↓ 调用 select(fd1, fd2, fd3...)
    → [阻塞] 等待任一 fd 就绪
    ← 返回就绪的 fd 列表
    ↓ 对每个就绪 fd 调用 read()
    → [阻塞] 复制数据(但已知有数据,几乎不卡)
✅ Java 示例(Selector):
Selector selector = Selector.open();
serverChannel.register(selector, OP_ACCEPT);

while (true) {
    selector.select(); // 阻塞,直到有事件
    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isReadable()) {
            SocketChannel ch = (SocketChannel) key.channel();
            ch.read(buffer); // 此时 read() 几乎不阻塞(因为已就绪)
        }
    }
}

✅ 优势:1 线程 → 10万+ 连接,Redis、Nginx、Netty 都基于此!


4️⃣ 信号驱动 I/O(Signal-driven I/O)—— 极少使用

  • 注册 SIGIO 信号处理函数
  • 阶段1:数据就绪时,内核发送 SIGIO 信号
  • 阶段2:应用在信号处理函数中调用 read()(仍阻塞)

🔸 Linux 支持,但 Java 无法直接使用(JVM 不暴露信号机制)
🔸 实际应用极少,了解即可


5️⃣ 异步 I/O(Asynchronous I/O)—— AIO

  • 阶段1 + 阶段2 全部由内核完成
  • 应用提交 aio_read() 后立即返回
  • 内核完成后主动通知(回调 or 事件)
  • 整个过程不阻塞、不轮询、不主动 read
🖼️ 流程图:
[应用线程]
    ↓ 调用 aio_read()
    → 立即返回!去做其他事
    → 内核:等数据 → 复制数据 → 完成
    → 内核触发回调 / 设置完成事件
    ← 应用在回调中拿到数据
✅ Java 示例(AIO):
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);

channel.read(buffer, null, new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(Integer result, Object attachment) {
        // 数据已自动读入 buffer!无需再 read()
        System.out.println("收到数据: " + new String(buffer.array(), 0, result));
    }
});
// 主线程继续执行,不阻塞!

⚠️ 现实:Linux 下 JDK AIO 是用 epoll + 线程池模拟的,并非真正的内核 AIO(io_uring)。
所以 Netty、Tomcat 等主流框架都不用 AIO


四、五大模型对比表

模型 阶段1(等数据) 阶段2(复制数据) 是否阻塞线程 是否需轮询 通知方式 Java 支持
阻塞 I/O 阻塞 阻塞 ✅(BIO)
非阻塞 I/O 立即返回 阻塞 ❌(但需轮询) ✅(NIO Channel)
I/O 多路复用 阻塞(在 select) 阻塞 ✅(但高效) select 返回 ✅(Selector)
信号驱动 I/O 信号通知 阻塞 SIGIO 信号
异步 I/O 内核完成 内核完成 回调/事件 ✅(AIO,但 Linux 不真异步)

五、Spring Boot 中如何选择?

场景 推荐 I/O 模型 技术栈
标准 Web API 多路复用(NIO) Tomcat(内嵌 NIO)
自定义 TCP 协议 多路复用(NIO) Netty
高频小包通信 多路复用(NIO) Netty + Epoll
文件异步读写 异步 I/O(AIO) AsynchronousFileChannel(谨慎使用)

📌 结论:99% 的高性能网络场景,选择“多路复用”(即 Netty)就对了!


六、常见误区澄清

❌ 误区1:“NIO = Non-blocking I/O = 异步”

错!
Java NIO 中的 “N” 是 “New”,不是 “Non-blocking”。
而且 NIO 默认是同步非阻塞 + 多路复用不是异步

❌ 误区2:“AIO 一定比 NIO 快”

错!
在 Linux 上,JDK AIO 性能往往不如 NIO(因为底层仍是线程池模拟)。
真正的异步 I/O(io_uring)在 JDK 20+ 才开始实验性支持。


七、总结

  • 阻塞 I/O:简单但低效,适合学习。
  • 非阻塞 I/O:需轮询,不实用。
  • I/O 多路复用高并发王者,Netty/Tomcat/Redis 的核心。
  • 异步 I/O:理想很美,现实骨感(Linux 支持差)。

记住:多路复用(Reactor 模式)是当前最成熟、最高效的 I/O 模型!


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐