BIO / NIO / AIO 对比
面试的时候,经常会被问到"BIO、NIO、AIO 有什么区别"。
大多数人能说出来: BIO 是阻塞的,NIO 是非阻塞的,AIO 是异步的。但追问一下"NIO 为什么是非阻塞"、"AIO 为什么是异步"、"它们各自适合什么场景",很多人就答不上来了。
今天我们把这个知识点彻底梳理清楚。
一、三种模型的核心区别
1.1 表格对比
1.2 【直观类比】
还是用餐厅点餐来类比:
- BIO:你站在柜台前等,服务员现场做,做好才给你。期间你只能站着等,什么也干不了。
- NIO:你拿到一个号码,自己每隔几秒去柜台问一下"好了吗?"——你被解放出来,可以坐着玩手机,但还得自己去问。
- AIO:你拿到号码,服务员说"好了我叫您"。你去玩手机,手机响了告诉你来取餐——你完全不用管。
1.3 工作原理图
BIO:
Thread ──→ accept() [阻塞] ──→ read() [阻塞] ──→ write() [阻塞] ──→ 结束
连接1
Thread ──→ accept() [阻塞] ──→ read() [阻塞] ──→ write() [阻塞] ──→ 结束
连接2
NIO:
Selector ──→ accept 事件 ──→ 处理
│
├───→ read 事件 ──→ 处理
│
└───→ write 事件 ──→ 处理
AIO:
发起异步读 ──→ 回调线程处理完成 ──→ 回调通知
二、代码对比
2.1 BIO:简单直接
// BIO 服务器
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // 阻塞
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int n = in.read(buf); // 阻塞
// 处理数据...
OutputStream out = socket.getOutputStream();
out.write(response);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
2.2 NIO:事件驱动
// NIO 服务器
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞,直到有事件就绪
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 处理 accept
}
if (key.isReadable()) {
// 处理 read
}
if (key.isWritable()) {
// 处理 write
}
}
}
2.3 AIO:回调驱动
// AIO 服务器
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
server.accept(null, this); // 继续接受
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, null, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 处理数据
}
});
}
});
// 主线程做别的事
while (true) { Thread.sleep(1000); }
三、适用场景分析
3.1 BIO 的适用场景
// 适合 BIO 的情况:
// 1. 连接数少(`<` 100)
// 2. 每个连接需要持续交互
// 3. 逻辑简单,延迟要求不高
// 4. 开发周期紧,代码要简单
优点:
- 代码简单直观,容易理解和调试
- 没有复杂的回调嵌套
- 适合业务逻辑简单的场景
缺点:
- 连接数增加时,线程数线性增长
- 线程资源消耗大
- CPU 利用率低(很多线程在等 IO)
3.2 NIO 的适用场景
// 适合 NIO 的情况:
// 1. 连接数多(> 1000)
// 2. 每个连接活跃度低(大部分时间在等数据)
// 3. 需要自己控制 IO 逻辑
// 4. 如:聊天服务器、长连接网关、代理服务器
优点:
- 单线程可以管理大量连接
- 线程资源消耗低
- 适合 IO 密集型场景
缺点:
- 代码复杂度较高
- 需要处理半包、粘包问题
- selectorKey 管理复杂
3.3 AIO 的适用场景
// 适合 AIO 的情况:
// 1. 连接数非常高(> 10000)
// 2. 需要极致性能
// 3. 能接受复杂的异步编程模型
// 4. 数据量不大的场景
优点:
- 真正的异步,代码更简洁
- 理论上性能最好
- 不需要自己轮询
缺点:
- 回调嵌套,代码难维护
- 调试困难
- Linux 早期实现并不完美
- 社区生态不如 NIO/Netty
四、性能对比
4.1 吞吐量对比
并发连接数 ──→ 吞吐量
BIO: / (很快达到瓶颈)
NIO: / (线性增长)
AIO: / (比 NIO 稍高)
实际上,在大多数场景下,NIO 和 AIO 的性能差距没有想象中大。因为真正的瓶颈往往在业务逻辑而不是 IO 模型。
4.2 延迟对比
4.3 内存消耗对比
假设每个线程栈 512KB,10000 连接:
BIO: 10000 线程 × 512KB = 5GB
NIO: 1 线程 + Buffer = 几十 MB
AIO: 回调线程池 + Buffer = 几十 MB
NIO 和 AIO 在内存消耗上差异不大,主要优势是远小于 BIO。
五、生产选型决策
5.1 选型决策树
需要支持多少并发连接?
│
├─ `<` 100 连接 ──→ BIO(简单够用)
│
├─ 100 ~ 10000 ──→ NIO 或 Netty(成熟稳定)
│
└─ > 10000 ──→ NIO/Netty 或 AIO(看团队能力)
5.2 实际案例
// 案例1:内部工具,10 个用户
// 选 BIO,简单够用
ServerSocket server = new ServerSocket(8080);
// 案例2:聊天服务器,10 万在线用户
// 选 Netty(基于 NIO),成熟生态
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 案例3:文件传输服务,高并发
// 可以考虑 NIO + sendfile 零拷贝
FileChannel.fromPath("largefile.txt").transferTo(...)
5.3 Netty vs 原生 NIO
很多同学问:既然 JDK 提供了 NIO/AIO,为什么还要用 Netty?
原生 NIO/AIO 问题: Netty 解决方案:
├─ API 复杂 ├─ 封装简单 API
├─ 半包粘包要自己处理 ├─ 内置编解码器
├─ BUG 多(History of NIO bugs)├─ 经过大量生产验证
├─ Buffer 管理麻烦 ├─ ByteBuf 自动管理
├─ 线程模型要自己写 ├─ 多种 Reactor 模型开箱即用
└─ 空轮询 Bug └─ 处理各种 Edge Case
💡
除非是对性能或内存有极致要求,或者在写底层库,否则直接用 Netty 是更务实的选择。它封装了 NIO 的复杂性,提供了更友好的 API 和更完善的生态。
六、面试追问链
第一层:基础区别
面试官问:"BIO、NIO、AIO 的区别是什么?"
BIO 是阻塞 IO,一个连接一个线程,线程在等待 IO 时什么也干不了;NIO 是非阻塞 IO,通过 Selector 轮询多个连接,只有就绪的才处理;AIO 是异步 IO,数据就绪后系统主动回调通知。
第二层:原理深入
面试官追问:"NIO 为什么能做到单线程管理多连接?"
依赖操作系统的 select/poll/epoll 机制。Channel 注册到 Selector 后,Selector 批量管理所有 Channel 的文件描述符。一次 select 调用可以检查所有 Channel 的状态,返回就绪的 Channel。线程不需要为每个连接都等待。
第三层:AIO 原理
面试官追问:"AIO 和 NIO 在实现上最大的区别是什么?"
NIO 的 read/write 调用仍然是同步的,Selector 只是告诉你"这个 Channel 可以读了",你还是要自己调用 read()。AIO 的 read/write 是异步的,返回时就告诉你成功了(或者失败),数据可能已经处理完了。
第四层:选型决策
面试官追问:"什么情况下你会选 BIO 而不是 NIO?"
当连接数少(<100)、业务逻辑简单、或者需要快速开发时,BIO 更合适。另外,如果团队不熟悉 NIO,强行上 NIO 可能引入更多问题。技术选型要结合实际情况,不是越"高级"越好。
【学习小结】
- BIO:简单但低效,适合低并发
- NIO:高效复杂,适合高并发,Netty 是主流选择
- AIO:真正的异步,但编程复杂,生态不如 NIO
- 选型要看连接数、团队能力、项目周期
- 大多数场景 NIO/Netty 是最优解