IO 模型

一、IO 模型核心基础

1. 核心概念

  • IO 本质:数据在「用户空间」和「内核空间」之间的传输过程
  • IO 两个阶段 (所有 IO 模型的核心区分点):
    1. 等待数据准备就绪(如网卡接收数据、磁盘读取数据到内核缓冲区)
    2. 将数据从内核空间拷贝到用户空间
  • 同步 / 异步 :关注「是否需要用户线程等待 IO 完成」
    • 同步:用户线程主动等待 IO 结果,全程参与
    • 异步:用户线程提交 IO 请求后即可做其他事,内核完成 IO 后主动通知
  • 阻塞 / 非阻塞 :关注「等待数据准备阶段是否阻塞线程」
    • 阻塞:线程挂起,直到数据准备完成
    • 非阻塞:线程轮询检查数据状态,未准备好时立即返回,不挂起

二、四大 IO 模型逐一解析

1. BIO(Blocking IO,阻塞式 IO)

(1)核心特点
  • 同步 + 阻塞,最基础的 IO 模型
  • 两个阶段均阻塞:等待数据准备阻塞 + 数据拷贝阻塞
  • 一个连接对应一个处理线程,线程利用率极低
(2)源码实战(Java BIO 服务端)

java

运行

复制代码
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO服务端示例:单线程处理,一个连接阻塞到底
 */
public class BIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 绑定端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO服务端启动,监听8080端口...");

        while (true) {
            // 2. 阻塞等待客户端连接(阶段1:等待连接准备)
            Socket socket = serverSocket.accept(); 
            System.out.println("客户端[" + socket.getInetAddress() + "]连接成功");

            // 3. 处理客户端请求(阻塞读取数据)
            new Thread(() -> {
                try (InputStream inputStream = socket.getInputStream()) {
                    byte[] buffer = new byte[1024];
                    while (true) {
                        // 4. 阻塞读取数据(阶段1:等待数据准备;阶段2:拷贝数据,均阻塞)
                        int readLen = inputStream.read(buffer); 
                        if (readLen == -1) break; // 客户端断开
                        System.out.println("收到数据:" + new String(buffer, 0, readLen));
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
(3)优缺点
  • 优点:实现简单、代码直观,适合低并发场景
  • 缺点:高并发下线程数爆炸(C10K 问题),线程上下文切换成本高

2. NIO(Non-Blocking IO,非阻塞 IO)

(1)核心特点
  • 同步 + 非阻塞
  • 等待数据准备阶段:线程轮询检查(非阻塞),数据未准备好时立即返回
  • 数据拷贝阶段:仍阻塞
  • 核心组件(Java NIO):Channel(双向通道)、Buffer(缓冲区)、Selector(多路复用器)
(2)源码实战(Java NIO 服务端)

java

运行

复制代码
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;
import java.util.Set;

/**
 * NIO服务端示例:基于Selector多路复用,单线程处理多连接
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 打开ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false); // 设置为非阻塞

        // 2. 打开Selector(多路复用器)
        Selector selector = Selector.open();
        // 3. 注册ServerSocketChannel到Selector,关注「连接就绪」事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务端启动,监听8080端口...");

        while (true) {
            // 4. 阻塞等待就绪事件(可设置超时时间,非阻塞模式可立即返回)
            selector.select(); // 无参数:阻塞;select(1000):超时1秒返回

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

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove(); // 处理完必须移除,避免重复处理

                // 6. 处理不同事件
                if (key.isAcceptable()) {
                    // 连接就绪:接受新连接
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false); // 非阻塞
                    // 注册新连接到Selector,关注「读就绪」事件
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("客户端[" + socketChannel.getRemoteAddress() + "]连接成功");
                } else if (key.isReadable()) {
                    // 读就绪:读取数据
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int readLen = socketChannel.read(buffer); // 非阻塞读取,无数据时返回0
                    if (readLen > 0) {
                        buffer.flip(); // 切换为读模式
                        System.out.println("收到数据:" + new String(buffer.array(), 0, buffer.limit()));
                        buffer.clear(); // 清空缓冲区
                    } else if (readLen == -1) {
                        // 客户端断开
                        key.cancel();
                        socketChannel.close();
                        System.out.println("客户端断开连接");
                    }
                }
            }
        }
    }
}
(3)优缺点
  • 优点:单线程处理多连接,解决 C10K 问题,线程利用率大幅提升
  • 缺点:轮询仍有开销,数据拷贝阶段仍阻塞,编程复杂度高于 BIO

3. IO 多路复用(Multiplexing IO)

(1)核心特点
  • 同步 + 非阻塞的升级版,是 NIO 的核心实现方式
  • 核心:由内核帮用户线程「监听多个连接的就绪状态」,无需用户线程轮询
  • 常见实现:Linux 下的 select/poll/epoll(epoll 是最优方案)
  • Java NIO 的 Selector 底层就是 epoll(Linux)/kqueue(Mac)/iocp(Windows)
(2)与纯 NIO 的区别
  • 纯 NIO:用户线程轮询每个连接的状态,开销随连接数增加而上升
  • 多路复用:内核批量监听所有连接,仅返回就绪的连接,开销可控

4. AIO(Asynchronous IO,异步 IO)

(1)核心特点
  • 异步 + 非阻塞
  • 两个阶段均由内核完成:用户线程提交 IO 请求后立即返回,内核完成「数据准备 + 拷贝」后,通过回调 / 通知告知用户线程
  • Java AIO(NIO.2)基于事件驱动,底层依赖操作系统异步 IO 支持(如 Linux 的 aio,Windows 的 IOCP)
(2)源码实战(Java AIO 服务端)

java

运行

复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * AIO服务端示例:异步IO,回调处理连接和数据
 */
public class AIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 打开异步ServerSocketChannel
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        System.out.println("AIO服务端启动,监听8080端口...");

        // 2. 异步接受连接(无阻塞,立即返回)
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            // 连接成功的回调
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                // 立即再次注册接受连接,避免漏接新连接
                serverChannel.accept(null, this);
                try {
                    System.out.println("客户端[" + socketChannel.getRemoteAddress() + "]连接成功");
                    // 3. 异步读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        // 读取数据成功的回调
                        @Override
                        public void completed(Integer readLen, ByteBuffer buffer) {
                            if (readLen > 0) {
                                buffer.flip();
                                System.out.println("收到数据:" + new String(buffer.array(), 0, buffer.limit()));
                                buffer.clear();
                                // 继续监听读事件
                                socketChannel.read(buffer, buffer, this);
                            } else if (readLen == -1) {
                                // 客户端断开
                                try {
                                    socketChannel.close();
                                    System.out.println("客户端断开连接");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }

                        // 读取数据失败的回调
                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                            try {
                                socketChannel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            // 连接失败的回调
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });

        // 防止主线程退出(AIO是异步的,主线程不阻塞)
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
(3)优缺点
  • 优点:完全异步,线程无任何阻塞,理论上性能最优
  • 缺点:编程复杂度高,操作系统支持程度不一(Linux 下 AIO 对文件 IO 支持较好,网络 IO 仍依赖 epoll),高并发下回调管理复杂

三、四大 IO 模型对比

表格

模型 同步 / 异步 阻塞 / 非阻塞 核心特点 适用场景
BIO 同步 阻塞 一连接一线程,简单但低效 低并发、简单业务(如小工具)
NIO(纯) 同步 非阻塞 轮询检查,开销随连接数上升 极少使用,被多路复用替代
IO 多路复用 同步 非阻塞 内核监听就绪事件,单线程多连接 高并发网络编程(如 Netty)
AIO 异步 非阻塞 内核完成全流程,回调通知 高并发、低延迟场景(如文件下载)

四、实战关键注意事项

  1. BIO 优化:可通过线程池(如 Tomcat BIO 模式)控制线程数,但本质仍未解决阻塞问题
  2. NIO 关键:Selector 的「空轮询」问题(JDKbug),可通过超时时间或替换实现(如 epoll)解决
  3. AIO 适用:Linux 下网络 IO 优先用 NIO 多路复用,文件 IO 可考虑 AIO;Windows 下 AIO(IOCP)表现更好
  4. 框架选择:实际开发优先用 Netty(基于 NIO 多路复用,封装了底层细节,解决了 NIO 的坑),而非手写原生 NIO/AIO

总结

  1. IO 模型的核心区分点是「数据准备阶段是否阻塞」和「是否需要用户线程等待 IO 完成」,同步异步关注全程,阻塞非阻塞关注数据准备阶段。
  2. BIO 是基础但低效,NIO 多路复用解决了 C10K 问题(主流方案),AIO 理论最优但落地成本高。
  3. 实际开发无需手写原生 IO 模型,优先选择 Netty 等成熟框架,底层依赖 NIO 多路复用即可满足绝大多数高并发场景。
相关推荐
你好潘先生7 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
Agent_大师8 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码8 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
copyer_xyf8 小时前
FastAPI 如何连接 MySQL
后端·python
apocelipes21 小时前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
用户8356290780511 天前
使用 Python 在 PDF 中创建与管理书签
后端·python
MeixianAgent1 天前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
咕白m6251 天前
用 Python 实现一键批量查找与替换 Excel 数据
后端·python
SelectDB2 天前
Apache Doris Python UDF:让 SQL 直接调用 Python 生态,支撑 Agent 时代复杂业务逻辑
大数据·数据库·python
荣码2 天前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python