BIO 到 NIO 再到 AIO:一篇文章彻底搞懂 I/O 的前世今生
一、先搞明白:什么是 IO?
IO就是四个字,数据搬运,通俗来讲就是 数据从外部进入计算机 或者 数据从计算机流向外部,网络上就是数据从客户端流向服务端。
例如:
text
磁盘读取文件
网络接收数据
数据库查询结果返回
键盘输入
本质都是:
text
数据搬运
例如:
java
String text = Files.readString(Path.of("test.txt"));
看起来只是:
java
读取文件
实际上发生的是:

数据经历了多次复制。
二、为什么 IO 很慢?
看一组经典数据:
| 操作 | 耗时 |
|---|---|
| CPU寄存器 | 1ns |
| L1缓存 | 0.5ns |
| 内存 | 100ns |
| SSD | 100μs |
| 网络 | 0.1ms~100ms |
换算:
text
CPU跑100万步
磁盘才动一下
所以:
程序真正慢的不是计算,而是等待IO。
三、一次 read() 到底发生了什么?
例如:
java
inputStream.read();
执行后:

这里出现两个问题:
问题1:
text
等待数据到来
问题2:
text
数据拷贝
Linux所有IO模型都在解决这两个问题。
四、Linux 五种 IO 模型
经典分类来自:
《UNIX Network Programming》
五种模型:
text
1 阻塞IO(Blocking IO)
2 非阻塞IO(Non Blocking IO)
3 IO复用(IO Multiplexing)
4 信号驱动IO
5 异步IO(Asynchronous IO)
Java的:
text
BIO
NIO
AIO
其实对应:
text
BIO → Blocking IO
NIO → IO Multiplexing
(select/poll/epoll)
AIO → Asynchronous IO
五、BIO:最符合人类思维的模型
代码:
java
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
System.out.println("开始等待客户端的连接。");
//阻塞方法
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接了");
new Thread(() -> {
try {
handler(clientSocket);
} catch (IOException e) {
System.out.println("发生IO错误");
}
}).start();
}
}
private static void handler(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("准备read客户端数据");
//接收客户端的数据,阻塞方法,没有数据可读时就阻塞
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕");
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
}
}
accept:
text
没有连接
↓
一直等待
↓
线程卡住
这就是:
text
Blocking IO
BIO到底阻塞在哪里?
很多人误解:
text
线程不工作了
实际上:
线程被挂起。
Linux:
c
accept()
执行:
text
进入内核
↓
连接没来
↓
线程加入等待队列
↓
CPU切换别的线程
线程状态:
text
RUNNING
↓
BLOCKED
BIO的问题
假设:
text
10000个连接
传统写法:
java
new Thread(() -> handle(socket))
形成:
text
1连接 -> 1线程
即:
text
C10K问题
10000线程意味着:
text
线程栈
上下文切换
CPU调度
内存消耗
全部爆炸。
六、NIO诞生
核心思想:一个线程管理多个连接,也就是非阻塞IO
NIO = 同步 + 非阻塞 + 多路复用
意思:线程仍然负责读取数据, 但是线程不等待某一个连接
核心变化:
BIO:
线程等连接、事件
NIO:
连接等事件
NIO 到底解决什么问题
假设 有100000连接 , 实际只有100连接活跃
BIO : 10万线程,严重浪费。
NIO:16线程 -> 处理10万连接
因为 线程 只处理:活跃连接
**NIO怎么实现活跃连接 **
IO复用
思想:让内核帮忙盯着,应用只问,哪些连接有数据?
内核维护:fd集合
用户:select(...)
流程:
text
用户态
↓
select
↓
内核检查10000连接
↓
返回活跃连接
优点:不用轮询不用10000线程
缺点:fd数量有限Linux只有1024个
poll 出现了
改进:
text
取消1024限制
但:
text
仍然遍历全部fd
复杂度:
text
O(n)
epoll革命
Linux 2.6后:
text
epoll
真正解决:
text
大量连接
核心思想:连接变化时通知,而不是 每次全部检查
结构:
text
红黑树
+
就绪链表
创建fd:
c
epoll_create()
注册:
c
epoll_ctl()
放入:
text
RBTree
数据到达:
text
网卡中断
↓
内核
↓
fd加入ReadyList
查询:
c
epoll_wait()
直接返回:
text
ReadyList
复杂度:
text
O(1)
NIO底层为什么快
Selector多路复用
java代码执行:
while(true){
fd = select();
for(fd){
read();
}
}
fd = select() 时linux:
text
epoll_ctl() // 处理事件
epoll_wait()// 等待事件
之后直接返回有数据的连接
差距:
text
O(n)
VS
O(1)
七、Java NIO架构
java
public static void main(String[] args) throws IOException {
// 创建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(8080));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功");
while (true) {
// 阻塞等待需要处理的事件发生
selector.select();
// 获取selector中注册的全部事件的 SelectionKey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历SelectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128);
int len = socketChannel.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
核心组件:Channel + Selector
Selector 与 linux:
- 打开Selector处理Channel:
java执行
Selector selector = Selector.open();
最终会走到
java
static native int wait(long h, long pollAddress, int numfds, int timeout)
throws IOException;
Linux 中进行:
创建fd:
epoll_create() // 创建文件描述符

- 把ServerSocketChannel注册到selector上
java执行
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
最终会走到
java
implRegister中
pollwrapper.add(fd) -- 也就是把上面创建的fd加到监听中去
- 阻塞等待需要处理的事件发生
java执行
selector.select();
最终走到
java
@Override
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
assert Thread.holdsLock(this);
// epoll_wait timeout is int
int to = (int) Math.min(timeout, Integer.MAX_VALUE);
boolean blocking = (to != 0);
int numEntries;
processUpdateQueue(); // 调用 WEPoll.ctl 处理 事件
processDeregisterQueue();
try {
begin(blocking);
boolean attempted = Blocker.begin(blocking);
try {
// 查询 事件
numEntries = WEPoll.wait(eph, pollArrayAddress, NUM_EPOLLEVENTS, to);
} finally {
Blocker.end(attempted);
}
} finally {
end(blocking);
}
processDeregisterQueue();
return processEvents(numEntries, action);
}
private void processUpdateQueue() {
assert Thread.holdsLock(this);
synchronized (updateLock) {
SelectionKeyImpl ski;
while ((ski = updateKeys.pollFirst()) != null) {
if (ski.isValid()) {
int fd = ski.getFDVal();
// add to fdToKey if needed
SelectionKeyImpl previous = fdToKey.putIfAbsent(fd, ski);
assert (previous == null) || (previous == ski);
int newOps = ski.translateInterestOps();
int registeredOps = ski.registeredEvents();
if (newOps != registeredOps) {
if (newOps == 0) {
// remove from epoll
WEPoll.ctl(eph, EPOLL_CTL_DEL, fd, 0);
} else {
int events = toEPollEvents(newOps);
if (registeredOps == 0) {
// add to epoll
WEPoll.ctl(eph, EPOLL_CTL_ADD, fd, events);
} else {
// modify events
WEPoll.ctl(eph, EPOLL_CTL_MOD, fd, events);
}
}
ski.registeredEvents(newOps);
}
}
}
}
}
java
// 调用 WEPoll.ctl 处理 事件
static native int ctl(long h, int opcode, long s, int events);
// 查询 事件
static native int wait(long h, long pollAddress, int numfds, int timeout)
throws IOException;
Linux 创建执行:
c
epoll_ctl()// 注册、修改、删除监听

c
epoll_wait()// 等待事件发生
八、AIO是什么?
NIO:
text
你主动问
AIO:
text
数据来了通知你
模型:
text
应用线程
│
提交请求
▼
Linux Kernel
│
读取数据
│
完成通知
▼
Callback
数据准备完成:
text
操作系统
↓
回调
↓
CompletionHandler(有completed和failed)
AIO与NIO本质区别
NIO:
text
read()
↓
线程等待
↓
返回数据
AIO:
text
aio_read()
↓
立刻返回
↓
内核处理
↓
回调用户
NIO:
text
Reactor
AIO:
text
Proactor
为什么 AIO不火
理论:
text
AIO > NIO
实际:
text
不一定
原因:
Linux 早期 AIO 很弱,没有真正成熟AIO。
传统 Linux AIO:仅文件支持较好、socket支持差
所以:
text
Redis、Nginx、Netty 默认仍然使用epoll
几乎没人用传统 AIO。
Linux新时代 -> 真正现代异步:io_uring
Linux 5.1:推出: io_uring
目标:
text
彻底异步
核心:
text
共享内存队列
结构:
Application
SQ
提交队列
↓
Kernel
↓
CQ
完成队列
执行:
提交
↓
线程立即返回
↓
内核执行
↓
完成事件
特点:
Shared Ring Buffer
用户态和内核共享队列
少系统调用
少上下文切换
真异步
支持网络
支持文件
支持零拷贝
九、Java AIO 底层是什么
Java:
AsynchronousSocketChannel
示例:
java
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
// 用于消费异步I/O操作结果的处理程序。这个包中定义的异步通道允许指定一个完成处理程序来使用异步操作的结果。 -- 也就是给操作系统准备的回调方法
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
// 这个 完成 方法在I/O操作成功完成时调用
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
serverChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
//这个 失败的 方法在I/O操作失败时调用。这些方法的实现应该及时完成,以避免使调用线程分派给其他完成处理程序。
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
完成:
completed()
Linux:
线程池模拟
+
epoll
不是纯内核 AIO。
所以: Java AIO 在 Linux 上很多时候:
AIO ≈ 线程池 + NIO
十、BIO、NIO、AIO终极对比
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| 模型 | 阻塞 | 同步非阻塞 | 异步 |
| 线程 | 一连接一线程 | 少量线程 | 更少线程 |
| 核心机制 | wait | epoll | callback |
| 编程复杂度 | 简单 | 中等 | 高 |
| 并发能力 | 低 | 高 | 极高 |
| Linux成熟度 | 极高 | 极高 | 一般 |
| 适用场景 | 小系统、调度系统 | 高并发服务、tomcat、Netty | 特殊场景 |
十一、一张图看懂整个IO演化史
text
BIO
│
│ 解决线程阻塞
▼
NIO
│
│ 解决连接爆炸
▼
epoll
│
│ 解决遍历成本
▼
AIO
│
│ 解决等待问题
▼
io_uring
结语
如果只记住一句话:
text
BIO、NIO、AIO 本质上是操作系统处理IO的三代思想:
BIO = 等
NIO = 问
AIO = 通知
