BIO / NIO / AIO 深度对比
mediumBIONIOAIOReactor多路复用Selector
本篇深入分析 BIO、NIO、AIO 的线程模型和 I/O 模型,搞清楚为什么 NIO 在高并发场景下优于 BIO,以及 Reactor 模式如何基于 NIO 构建。
从操作系统说起:I/O 的五种模型
Unix/Linux 定义了五种 I/O 模型(Richard Stevens《UNIX 网络编程》):
| 模型 | 等待数据 | 从内核拷贝数据 |
|---|---|---|
| 阻塞 I/O(BIO) | 阻塞等 | 阻塞等 |
| 非阻塞 I/O | 立即返回,轮询 | 阻塞等 |
| I/O 多路复用(select/epoll) | 阻塞在 select | 阻塞等 |
| 信号驱动 I/O | 信号通知 | 阻塞等 |
| 异步 I/O(AIO) | 不等 | 不等,完成后通知 |
Java 的:
- BIO = 阻塞 I/O
- NIO = I/O 多路复用(epoll/kqueue)
- AIO = 异步 I/O(Windows IOCP;Linux 上实际是线程池模拟)
BIO:一连接一线程
线程模型
客户端 1 ─── Socket ───┐
├─ accept()
客户端 2 ─── Socket ───┤
├─ accept()
客户端 N ─── Socket ───┘
每个 Socket 分配一个线程:
Thread-1 ─── read/write Socket 1(阻塞等待读写完成)
Thread-2 ─── read/write Socket 2(阻塞等待读写完成)
Thread-N ─── read/write Socket N(阻塞等待读写完成)
核心代码模型
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞:等待新连接
new Thread(() -> { // 为每个连接创建新线程
try (InputStream in = socket.getInputStream()) {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) { // 阻塞:等待数据
process(buf, 0, len);
}
}
}).start();
}
BIO 的问题
- 线程数 = 并发连接数,C10K(1 万并发)需要 1 万个线程
- 线程是重资源(JVM 默认每个线程 512KB~1MB 栈),内存消耗大
- 大量线程引发上下文切换开销
- 大多数时候线程在空等(网络 I/O 速度远慢于 CPU)
结论:BIO 适合连接数少、连接时间短的场景(如命令行工具),不适合高并发服务器。
NIO:多路复用,一线程处理多连接
核心思想
用 Selector(选择器) 监视多个 Channel,哪个 Channel 就绪(有数据可读/可写)就处理哪个,一个线程可以管理成千上万个连接。
Thread-1 持有 Selector
│
Selector.select()──等待任意一个 Channel 就绪
│
├─ Channel 1 可读?→ 读取数据
├─ Channel 2 可写?→ 写入数据
└─ Channel 3 可读?→ 读取数据
│
继续循环 select()
NIO 核心代码(单线程 Reactor)
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 非阻塞模式
server.register(selector, SelectionKey.OP_ACCEPT); // 监听 accept 事件
while (true) {
selector.select(); // 阻塞,直到有至少一个 Channel 就绪
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
// 有新连接
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 数据就绪,可读
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer); // 此时不会阻塞(有数据才调用)
buffer.flip();
process(buffer);
}
}
}
SelectionKey 的四种事件
| 事件 | 常量 | 含义 |
|---|---|---|
| OP_ACCEPT | 16 | 有新连接请求 |
| OP_CONNECT | 8 | 连接建立完成 |
| OP_READ | 1 | 通道有数据可读 |
| OP_WRITE | 4 | 通道可以写入 |
底层:epoll(Linux)
NIO 的 Selector 在 Linux 上基于 epoll 实现:
select(旧,O(n)): 遍历所有 fd 检查是否就绪
↓ 改进
poll(中,O(n)): 解决 fd 数量限制,但仍然 O(n)
↓ 改进
epoll(新,O(1)): 只返回就绪的 fd,内核通知,而不是轮询
epoll 核心 API:
epoll_create():创建 epoll 实例epoll_ctl():注册/删除感兴趣的 fdepoll_wait():等待就绪事件(阻塞,类似 Selector.select())
epoll 的优势:
- O(1) 复杂度:不管注册了多少 fd,每次只处理就绪的
- 边缘触发(ET)/水平触发(LT):灵活的触发模式
- 内核空间维护就绪列表:不需要用户空间轮询
Reactor 模式
NIO 的使用需要一个设计模式来组织代码:Reactor 模式。
单线程 Reactor
Client
─────────────► Reactor(单线程)
│
select()
│
┌───────────┼──────────┐
▼ ▼ ▼
Accept ReadHandler WriteHandler
- 一个线程做所有事
- 问题:Handler 业务逻辑复杂时阻塞 Reactor,影响所有连接
多线程 Reactor(主流)
mainReactor(Accept 连接)
│
sub-Reactor 线程池(处理读写)
├─ sub-Reactor 1 → WorkerPool 线程 → 业务处理
├─ sub-Reactor 2 → WorkerPool 线程 → 业务处理
└─ sub-Reactor N → WorkerPool 线程 → 业务处理
Netty 的线程模型正是基于此:
- BossGroup(1~2 个线程):专门 accept 连接
- WorkerGroup(CPU 核数 × 2 个线程):处理读写和业务
AIO:操作系统主动回调
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel channel, Void attachment) {
server.accept(null, this); // 继续接受下一个
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// I/O 完成,操作系统主动调用
buffer.flip();
process(buffer);
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) { }
});
}
@Override
public void failed(Throwable exc, Void attachment) { }
});
AIO 的问题:
- Linux 的
io_uring(2019 年引入)才算真正的异步 I/O - Java AIO 在 Linux 上底层是线程池模拟异步,并没有真正减少系统调用
- Netty 团队测试后决定不支持 AIO,认为 NIO 的 epoll 更高效
三者选择指南
连接数 < 1000,业务简单 → BIO(代码最简单)
连接数 1000+,网络 I/O 密集 → NIO(Netty 等框架封装)
大文件异步读写(Windows) → AIO(Windows IOCP 真正异步)
实际上:几乎所有高性能 Java 服务器框架(Netty、Vert.x、Undertow)都基于 NIO,很少看到 AIO 落地。