Skip to content

IO 面试题: BIO、NIO、AIO 一次说清

IO 模型是 Java 面试的高频题,很多人背了一堆概念,遇到实际场景还是懵。

先搞清楚一个根本问题:IO 等待时,CPU 在干什么?

答案是:干等着。所以要让 CPU 去做别的事。

三种 IO 模型

BIO(同步阻塞)

传统 IO,read 时线程阻塞在那儿等数据:

java
// 服务端:为每个连接分配一个线程
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket socket = server.accept(); // 阻塞等连接
    new Thread(() -> {
        InputStream in = socket.getInputStream();
        // read 阻塞:等数据
        int data = in.read();
    }).start();
}

10000 个连接 → 10000 个线程 → 机器直接挂掉。

NIO(同步非阻塞)

一个线程轮询所有连接,有数据了才处理:

java
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (selector.select() > 0) { // 阻塞,但只阻塞 selector
    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isAcceptable()) {
            // 处理新连接
        } else if (key.isReadable()) {
            // 处理读事件
        }
    }
    selector.selectedKeys().clear();
}

AIO(异步 IO)

操作系统内核完成 IO 后通知你,连轮询都省了:

java
AsynchronousServerSocketChannel server =
    AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    @Override
    public void completed(AsynchronousSocketChannel ch, Void attachment) {
        // 读完了回调这里
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        ch.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer buf) {
                // 处理数据
            }
        });
    }
});

核心组件对比

组件BIONIOAIO
连接SocketSocketChannelAsynchronousSocketChannel
数据载体StreamBufferByteBuffer
统一管理Selector
等待方式阻塞select 轮询内核回调

文件复制:传统 IO vs NIO

java
// 传统 BIO:边读边写,频繁上下文切换
try (FileInputStream in = new FileInputStream("src.txt");
     FileOutputStream out = new FileOutputStream("dst.txt")) {
    byte[] buf = new byte[8192];
    int len;
    while ((len = in.read(buf)) != -1) {
        out.write(buf, 0, len);
    }
}

// NIO:transferTo 直接在内核态复制,零拷贝
try (FileChannel in = new FileInputStream("src.txt").getChannel();
     FileChannel out = new FileOutputStream("dst.txt").getChannel()) {
    long size = in.size();
    long transferred = 0;
    while (transferred < size) {
        transferred += in.transferTo(transferred, size - transferred, out);
    }
}

选择建议

场景推荐原因
低并发(<1000)BIO简单够用
高并发短连接NIO单线程处理大量连接
高并发长连接NIO / AIO减少线程开销
文件操作NIO transferTo零拷贝绕过堆外内存

常见追问

Q: NIO 为什么比 BIO 性能好?

BIO 的问题是:每个连接一个线程,线程切换成本高。NIO 用一个 selector 线程管理所有连接,只有真正有事件时才处理。

Q: 什么是零拷贝?

传统 IO 要经历:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡。零拷贝跳过用户缓冲区,直接内核之间传输,减少两次 CPU 拷贝。

理解 IO 模型的核心:让 CPU 在等待 IO 时去做别的事,而不是干等着。

基于 VitePress 构建