AIO、BIO 和 NIO 的区别?

#Java#面试八股

AIO、BIO 和 NIO 的区别?

结论:三者的本质区别是**"谁在等数据"**。BIO(blocking IO)同步阻塞,一个连接占一个线程,读不到数据线程就挂起;NIO(non-blocking IO)同步非阻塞,用 Selector 做多路复用(multiplexing),一个线程轮询管理成千上万个连接,就绪后再处理;AIO(asynchronous IO,也称 NIO.2)异步非阻塞,发起读写后立即返回,由操作系统完成后回调 CompletionHandler,应用线程全程不等待。

1. 三种模型的线程视图

BIO:  client1 ──→ thread1(阻塞在 read)
       client2 ──→ thread2(阻塞在 read)
       client3 ──→ thread3(阻塞在 read)        连接数 ↑ ⇒ 线程数 ↑

NIO:  client1 ─┐
       client2 ─┼──→ Selector(轮询就绪事件)──→ 单线程分发 ──→ 业务线程池
       client3 ─┘        一个线程管 N 个连接

AIO:  client ──→ 发起 read 并注册 CompletionHandler ──→ 立即返回
                 内核完成读 ──→ 回调线程执行 completed()

2. 逐项对比

维度BIONIOAIO
IO 模型同步阻塞同步非阻塞 + 多路复用异步非阻塞
核心 APIInputStream/OutputStream、SocketChannel、Buffer、SelectorAsynchronousSocketChannel、CompletionHandler
线程模型1 连接 1 线程1 个 selector 线程 + 少量 worker回调线程池
编程复杂度低高(半包、粘包、状态机)中(回调嵌套)
吞吐能力连接数少时够用高并发首选理论上更高,生态弱
典型应用传统 ServerSocket、JDBCNetty、Redis 客户端、Tomcat NIO少量文件/网络异步场景

3. 代码:NIO 的服务端骨架

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NioServer {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));
        server.configureBlocking(false);                 // 必须非阻塞才能注册
        server.register(selector, SelectionKey.OP_ACCEPT);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (true) {
            selector.select();                           // 阻塞直到有就绪事件
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();                             // 必须移除,否则重复处理
                if (key.isAcceptable()) {
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    buf.clear();
                    int n = client.read(buf);            // 非阻塞:可能读到 0 字节
                    if (n == -1) { client.close(); continue; }
                    buf.flip();                          // 写模式切读模式
                    // 业务处理:注意 TCP 是字节流,要自己处理半包
                }
            }
        }
    }
}

对应的 BIO 版本简单得多,但每个连接都要 new Thread 或交给线程池,连接数一多线程就被耗尽。

4. 怎么选

场景推荐理由
连接数少(几十个)、逻辑简单BIO + 线程池代码直观,排查容易
高并发长连接(IM、网关、推送)NIO(一般直接用 Netty)少量线程支撑大量连接
大量小文件异步读写AIO / CompletableFuture避免线程阻塞在 IO
内网 RPC、低延迟NIO + 池化复用连接、减少握手
传统 Web 应用Tomcat NIO connector容器已封装好多路复用

5. 常见追问

追问回答要点
同步/异步、阻塞/非阻塞怎么区分?阻塞指调用是否立即返回;同步指结果是否由调用方主动获取。NIO 是同步非阻塞(要自己 select 再读),AIO 是异步(内核通知)
为什么 Netty 不用 AIO?Linux 上 JDK 的 AIO 实现成熟度与性能都不如 epoll 多路复用,且调试困难
多路复用底层是什么?Linux epoll、macOS kqueue、Windows IOCP;JDK 通过 SelectorProvider 适配
NIO 就一定会快吗?不一定,连接少时多路复用反而多一次事件分发;它解决的是"连接数"而非"单连接延迟"
为什么 NIO 要处理半包/粘包?TCP 是字节流,没有消息边界,需要长度字段或分隔符切分
BIO 的"伪异步"是什么?用线程池限制线程数、请求排队,避免线程无限增长,但阻塞本质未变

小结

BIO 用线程换简单,NIO 用事件换并发,AIO 用回调换等待。工程上几乎不会手写 NIO,而是用 Netty;面试要讲清 "Selector 多路复用 + 非阻塞 Channel" 这条主线,以及 BIO 在高连接数下线程耗尽的必然性。

笔记加载中…