一文看懂 BIO、NIO、AIO
IO模型对比:BIO、NIO、AIO 本文通过餐馆经营类比,通俗易懂地解释了三种IO模型的核心区别: BIO(同步阻塞):一对一服务模式,每个请求分配独立线程处理,线程在IO操作时阻塞。适合低并发场景,但资源消耗大。 NIO(同步非阻塞):多路复用模式,单线程通过选择器监控多个通道事件,实现非阻塞处理。核心组件包括Channel、Buffer和Selector。 AIO(异步非阻塞):回调通知模式
一文看懂 BIO、NIO、AIO
想象一下,你开了一家小餐馆,负责接待客人和上菜。客人就像网络请求,你处理请求的方式,就像程序处理 IO 操作的不同模式。今天就用餐馆经营做比喻,聊聊 BIO、NIO、AIO 这三种 IO 模型的区别。
先搞懂两个关键概念:同步/异步 和 阻塞/非阻塞
在讲三种模型前,得先明白这两组词的意思,它们是区分 IO 模型的核心:
-
同步 vs 异步:看的是"结果怎么来"
- 同步:你得主动去问结果(比如点餐后站在柜台等餐)
- 异步:结果自动来找你(比如外卖到了骑手打电话通知你)
-
阻塞 vs 非阻塞:看的是"等待时能不能干别的"
- 阻塞:等待时啥也干不了(比如打电话时对方让等,你只能举着电话发呆)
- 非阻塞:等待时可以做其他事(比如发微信后不用等回复,先去刷视频)
有了这两个标尺,我们就能给三种 IO 模型"贴标签"了:
- BIO:同步阻塞
- NIO:同步非阻塞
- AIO:异步非阻塞
一、BIO:就像"一个客人配一个服务员"
BIO(阻塞 IO)是最传统的处理方式,就像小餐馆刚开业时的经营模式:
工作场景
你雇了几个服务员(线程),来了一个客人(请求),就派一个服务员全程跟进。服务员带客人入座、点餐、传菜,直到客人离开。如果客人临时去打电话(IO 操作未完成),服务员就只能站在旁边等着,啥也干不了。
代码里的表现
用 Java 早期的 Socket
编程就是典型的 BIO:
- 服务器用
accept()
方法等客人来,没人来就一直等(阻塞) - 来了客人就派一个线程专门服务,线程调用
read()
读数据时,如果对方没发数据,线程就卡在那不动(阻塞)
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建服务器套接字,绑定到8080端口
// 相当于餐馆租了个店面,门口挂了个牌子"8080号店"
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO服务器启动,监听端口8080...");
// 2. 创建线程池,管理服务员(线程)
// 避免来了客人再临时雇人,提前准备好一批服务员
ExecutorService executor = Executors.newCachedThreadPool();
while (true) {
// 3. 等待客户端连接(阻塞方法)
// 服务员站在门口等客人,没人来就一直等,啥也干不了
final Socket clientSocket = serverSocket.accept();
System.out.println("新客人来了,分配一个服务员...");
// 4. 为每个新连接分配一个线程处理(一客一线程)
executor.execute(() -> {
try {
// 5. 获取输入流,准备读取数据(客人点餐)
InputStream in = clientSocket.getInputStream();
byte[] buffer = new byte[1024];
while (true) {
// 6. 读取数据(阻塞方法)
// 如果客人还没点餐(没发数据),服务员就站着等
int len = in.read(buffer);
if (len == -1) { // 客人说"点完了"
break;
}
// 处理客人的请求
System.out.println("收到客人消息:" + new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 7. 客人离开,关闭连接
clientSocket.close();
System.out.println("客人走了,服务员空闲了");
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
问题在哪?
如果突然来了 100 个客人(高并发),你就得雇 100 个服务员(线程)。但服务员工资贵啊(线程占用内存),人多了还会互相干扰(CPU 上下文切换)。而且大部分时间服务员都在"站着等"(IO 阻塞),纯属浪费人力。
适合啥情况?
就像社区小餐馆,每天就几桌客人,用这种模式简单好管理。程序里就是并发量特别低的场景,比如内部小工具。
二、NIO:就像"一个服务员管整个餐厅"
NIO(非阻塞 IO)就像餐馆扩大规模后的聪明做法:雇一个全能服务员(单线程),通过"望闻问切"同时处理所有客人。
工作场景
这个服务员超能干:
- 先站在门口看有没有新客人(用 Selector 监听连接事件)
- 看到新客人就安排入座,然后继续盯着门口和其他桌子
- 时不时扫一眼各桌:有客人举手要点餐(读数据)就过去记单,有客人吃完要结账(写数据)就过去处理
- 如果某桌客人正在打电话(数据没准备好),服务员不傻等,先去忙别的桌子
代码里的关键角色
NIO 有三个核心"工具":
- Channel(通道):就像每张餐桌,是和客人交互的场所(替代 BIO 里的 Socket)
- Buffer(缓冲区):就像服务员的记事本,记着客人点的菜(数据必须先放这里才能处理)
- Selector(选择器):就像服务员的"火眼金睛",能同时盯着所有桌子的动态(哪个需要服务)
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;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建服务器通道(相当于餐厅大门)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定8080端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式(关键!服务员不会傻等)
serverSocketChannel.configureBlocking(false);
// 2. 创建选择器(相当于服务员的"火眼金睛")
// 能同时盯着所有桌子(通道)的动态
Selector selector = Selector.open();
// 3. 将服务器通道注册到选择器,关注"新客人到了"事件(OP_ACCEPT)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器启动,监听端口8080...");
while (true) {
// 4. 等待事件发生(阻塞方法,但可以设置超时时间)
// 服务员站在餐厅中央观察,没人需要服务就稍等
int readyChannels = selector.select();
if (readyChannels == 0) {
continue; // 没人需要服务,继续观察
}
// 5. 获取所有有事件发生的通道(需要服务的桌子)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 6. 处理"新客人到了"事件
if (key.isAcceptable()) {
// 获取服务器通道
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受新连接(非阻塞,因为前面已经知道有客人来了)
SocketChannel clientChannel = serverChannel.accept();
System.out.println("新客人来了,安排入座...");
// 客户端通道也要设置为非阻塞
clientChannel.configureBlocking(false);
// 将客户端通道注册到选择器,关注"客人要点餐"事件(OP_READ)
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 7. 处理"客人要点餐"事件(有数据可读)
else if (key.isReadable()) {
// 获取客户端通道
SocketChannel clientChannel = (SocketChannel) key.channel();
// 获取缓冲区(服务员的记事本)
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 读取数据(非阻塞,因为前面已经知道有数据了)
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
// 切换缓冲区为读模式
buffer.flip();
// 读取数据并处理
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到客人消息:" + new String(data));
// 清空缓冲区,准备下次使用
buffer.clear();
} else if (bytesRead == -1) {
// 客人离开了,关闭通道
clientChannel.close();
System.out.println("客人走了");
}
}
// 8. 移除已处理的事件,避免重复处理
keyIterator.remove();
}
}
}
}
为啥比 BIO 好?
一个服务员就能管几十桌客人(单线程处理多连接),人力成本骤降。服务员大部分时间都在"主动干活",而不是"傻等",效率高多了。就像酒吧的调酒师,同时盯着好几个酒杯,哪个该加料了就过去处理一下。
适合啥情况?
就像生意火爆的连锁餐厅,客人多但 each 客人停留时间不长。程序里就是高并发场景,比如聊天软件服务器、消息队列(Kafka 就用了类似思想)。
三、AIO:就像"全自动智能餐厅"
AIO(异步 IO)是最高级的模式,相当于把餐厅改成了全自动模式:
工作场景
你雇了个前台(线程),客人来了只需在前台登记需求,然后就可以随便逛(线程继续干别的)。餐厅里有各种智能设备:
- 客人扫码点餐,系统自动通知后厨(IO 操作由系统处理)
- 菜做好了,传送带自动把菜送到桌前,同时给客人发消息:“您的餐到了”(IO 完成后系统回调通知)
- 整个过程中,前台不用管任何细节,客人也不用等
代码里的表现
Java 7 后的 AsynchronousSocketChannel
就是 AIO:
- 发起 IO 操作时,只需要告诉系统:“完事了叫我”(注册回调函数)
- 线程该干啥干啥,完全不耽误
- 系统处理完 IO 后,自动调用你写的回调方法
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;
import java.util.concurrent.CountDownLatch;
public class AIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("AIO服务器启动,监听端口8080...");
// 1. 创建异步服务器通道(全自动餐厅的入口)
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
// 绑定8080端口
serverChannel.bind(new InetSocketAddress(8080));
// 用于等待,防止主线程退出
CountDownLatch latch = new CountDownLatch(1);
// 2. 接受连接(异步操作)
// 告诉系统:"有人来的时候叫我,我先去忙别的了"
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
// 3. 连接建立成功后的回调方法(客人到了,系统通知)
@Override
public void completed(AsynchronousSocketChannel clientChannel, Object attachment) {
System.out.println("新客人来了,系统已自动安排...");
// 继续接受其他连接(告诉系统:下一个客人来的时候也叫我)
serverChannel.accept(null, this);
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4. 异步读取数据(告诉系统:客人点餐完成后叫我)
clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
// 5. 读取完成后的回调方法(客人点完餐,系统通知)
@Override
public void completed(Integer bytesRead, ByteBuffer buffer) {
if (bytesRead > 0) {
// 切换缓冲区为读模式
buffer.flip();
// 读取数据并处理
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到客人消息:" + new String(data));
// 可以在这里处理业务逻辑,然后异步写回响应
// 清空缓冲区,准备下次读取
buffer.clear();
// 继续监听这个客人的新消息
clientChannel.read(buffer, buffer, this);
} else if (bytesRead == -1) {
// 客人离开了
try {
clientChannel.close();
System.out.println("客人走了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 读取失败的回调方法
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
// 连接建立失败的回调方法
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
latch.countDown();
}
});
// 等待,保持服务器运行
latch.await();
}
}
真的完美吗?
理论上效率最高,但现实中有点"水土不服":
- 智能设备成本高(操作系统对异步 IO 的支持不完善,尤其 Linux 环境)
- 出了问题不好排查(回调逻
- 辑复杂,调试麻烦)
- 就像自动餐厅,偶尔会出现"菜送到了但客人没收到通知"的 bug
适合啥情况?
就像需要长时间烹饪的高端餐厅(IO 操作耗时久),比如大文件传输、数据库慢查询,这时全自动模式才能体现优势。
三者对比:一张表看懂核心差异
对比项 | BIO(同步阻塞) | NIO(同步非阻塞) | AIO(异步非阻塞) |
---|---|---|---|
形象比喻 | 一个客人配一个服务员 | 一个服务员管所有客人 | 全自动餐厅,客人自助+系统通知 |
线程状态 | 大部分时间在傻等 | 主动巡视,不等闲 | 彻底解放,啥也不用等 |
并发能力 | 弱(100 人需要 100 个服务员) | 强(1 人能管 1000 人) | 极强(理论无上限) |
编程难度 | 简单(像写流水账) | 中等(要懂"望闻问切") | 难(要设计回调逻辑) |
典型应用 | 早期小网站 | Netty 框架、Kafka | 大文件传输、数据库异步操作 |
更多推荐
所有评论(0)