一、IO 模型核心基础
1. 核心概念
- IO 本质:数据在「用户空间」和「内核空间」之间的传输过程
- IO 两个阶段 (所有 IO 模型的核心区分点):
- 等待数据准备就绪(如网卡接收数据、磁盘读取数据到内核缓冲区)
- 将数据从内核空间拷贝到用户空间
- 同步 / 异步 :关注「是否需要用户线程等待 IO 完成」
- 同步:用户线程主动等待 IO 结果,全程参与
- 异步:用户线程提交 IO 请求后即可做其他事,内核完成 IO 后主动通知
- 阻塞 / 非阻塞 :关注「等待数据准备阶段是否阻塞线程」
- 阻塞:线程挂起,直到数据准备完成
- 非阻塞:线程轮询检查数据状态,未准备好时立即返回,不挂起
二、四大 IO 模型逐一解析
1. BIO(Blocking IO,阻塞式 IO)
(1)核心特点
- 同步 + 阻塞,最基础的 IO 模型
- 两个阶段均阻塞:等待数据准备阻塞 + 数据拷贝阻塞
- 一个连接对应一个处理线程,线程利用率极低
(2)源码实战(Java BIO 服务端)
java
运行
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服务端示例:单线程处理,一个连接阻塞到底
*/
public class BIOServer {
public static void main(String[] args) throws IOException {
// 1. 绑定端口
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO服务端启动,监听8080端口...");
while (true) {
// 2. 阻塞等待客户端连接(阶段1:等待连接准备)
Socket socket = serverSocket.accept();
System.out.println("客户端[" + socket.getInetAddress() + "]连接成功");
// 3. 处理客户端请求(阻塞读取数据)
new Thread(() -> {
try (InputStream inputStream = socket.getInputStream()) {
byte[] buffer = new byte[1024];
while (true) {
// 4. 阻塞读取数据(阶段1:等待数据准备;阶段2:拷贝数据,均阻塞)
int readLen = inputStream.read(buffer);
if (readLen == -1) break; // 客户端断开
System.out.println("收到数据:" + new String(buffer, 0, readLen));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
(3)优缺点
- 优点:实现简单、代码直观,适合低并发场景
- 缺点:高并发下线程数爆炸(C10K 问题),线程上下文切换成本高
2. NIO(Non-Blocking IO,非阻塞 IO)
(1)核心特点
- 同步 + 非阻塞
- 等待数据准备阶段:线程轮询检查(非阻塞),数据未准备好时立即返回
- 数据拷贝阶段:仍阻塞
- 核心组件(Java NIO):Channel(双向通道)、Buffer(缓冲区)、Selector(多路复用器)
(2)源码实战(Java NIO 服务端)
java
运行
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;
/**
* NIO服务端示例:基于Selector多路复用,单线程处理多连接
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 打开ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 设置为非阻塞
// 2. 打开Selector(多路复用器)
Selector selector = Selector.open();
// 3. 注册ServerSocketChannel到Selector,关注「连接就绪」事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务端启动,监听8080端口...");
while (true) {
// 4. 阻塞等待就绪事件(可设置超时时间,非阻塞模式可立即返回)
selector.select(); // 无参数:阻塞;select(1000):超时1秒返回
// 5. 获取就绪的事件集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 处理完必须移除,避免重复处理
// 6. 处理不同事件
if (key.isAcceptable()) {
// 连接就绪:接受新连接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false); // 非阻塞
// 注册新连接到Selector,关注「读就绪」事件
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端[" + socketChannel.getRemoteAddress() + "]连接成功");
} else if (key.isReadable()) {
// 读就绪:读取数据
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int readLen = socketChannel.read(buffer); // 非阻塞读取,无数据时返回0
if (readLen > 0) {
buffer.flip(); // 切换为读模式
System.out.println("收到数据:" + new String(buffer.array(), 0, buffer.limit()));
buffer.clear(); // 清空缓冲区
} else if (readLen == -1) {
// 客户端断开
key.cancel();
socketChannel.close();
System.out.println("客户端断开连接");
}
}
}
}
}
}
(3)优缺点
- 优点:单线程处理多连接,解决 C10K 问题,线程利用率大幅提升
- 缺点:轮询仍有开销,数据拷贝阶段仍阻塞,编程复杂度高于 BIO
3. IO 多路复用(Multiplexing IO)
(1)核心特点
- 同步 + 非阻塞的升级版,是 NIO 的核心实现方式
- 核心:由内核帮用户线程「监听多个连接的就绪状态」,无需用户线程轮询
- 常见实现:Linux 下的 select/poll/epoll(epoll 是最优方案)
- Java NIO 的 Selector 底层就是 epoll(Linux)/kqueue(Mac)/iocp(Windows)
(2)与纯 NIO 的区别
- 纯 NIO:用户线程轮询每个连接的状态,开销随连接数增加而上升
- 多路复用:内核批量监听所有连接,仅返回就绪的连接,开销可控
4. AIO(Asynchronous IO,异步 IO)
(1)核心特点
- 异步 + 非阻塞
- 两个阶段均由内核完成:用户线程提交 IO 请求后立即返回,内核完成「数据准备 + 拷贝」后,通过回调 / 通知告知用户线程
- Java AIO(NIO.2)基于事件驱动,底层依赖操作系统异步 IO 支持(如 Linux 的 aio,Windows 的 IOCP)
(2)源码实战(Java AIO 服务端)
java
运行
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;
/**
* AIO服务端示例:异步IO,回调处理连接和数据
*/
public class AIOServer {
public static void main(String[] args) throws IOException {
// 1. 打开异步ServerSocketChannel
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
System.out.println("AIO服务端启动,监听8080端口...");
// 2. 异步接受连接(无阻塞,立即返回)
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
// 连接成功的回调
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
// 立即再次注册接受连接,避免漏接新连接
serverChannel.accept(null, this);
try {
System.out.println("客户端[" + socketChannel.getRemoteAddress() + "]连接成功");
// 3. 异步读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
// 读取数据成功的回调
@Override
public void completed(Integer readLen, ByteBuffer buffer) {
if (readLen > 0) {
buffer.flip();
System.out.println("收到数据:" + new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
// 继续监听读事件
socketChannel.read(buffer, buffer, this);
} else if (readLen == -1) {
// 客户端断开
try {
socketChannel.close();
System.out.println("客户端断开连接");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 读取数据失败的回调
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
// 连接失败的回调
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
// 防止主线程退出(AIO是异步的,主线程不阻塞)
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(3)优缺点
- 优点:完全异步,线程无任何阻塞,理论上性能最优
- 缺点:编程复杂度高,操作系统支持程度不一(Linux 下 AIO 对文件 IO 支持较好,网络 IO 仍依赖 epoll),高并发下回调管理复杂
三、四大 IO 模型对比
表格
| 模型 | 同步 / 异步 | 阻塞 / 非阻塞 | 核心特点 | 适用场景 |
|---|---|---|---|---|
| BIO | 同步 | 阻塞 | 一连接一线程,简单但低效 | 低并发、简单业务(如小工具) |
| NIO(纯) | 同步 | 非阻塞 | 轮询检查,开销随连接数上升 | 极少使用,被多路复用替代 |
| IO 多路复用 | 同步 | 非阻塞 | 内核监听就绪事件,单线程多连接 | 高并发网络编程(如 Netty) |
| AIO | 异步 | 非阻塞 | 内核完成全流程,回调通知 | 高并发、低延迟场景(如文件下载) |
四、实战关键注意事项
- BIO 优化:可通过线程池(如 Tomcat BIO 模式)控制线程数,但本质仍未解决阻塞问题
- NIO 关键:Selector 的「空轮询」问题(JDKbug),可通过超时时间或替换实现(如 epoll)解决
- AIO 适用:Linux 下网络 IO 优先用 NIO 多路复用,文件 IO 可考虑 AIO;Windows 下 AIO(IOCP)表现更好
- 框架选择:实际开发优先用 Netty(基于 NIO 多路复用,封装了底层细节,解决了 NIO 的坑),而非手写原生 NIO/AIO
总结
- IO 模型的核心区分点是「数据准备阶段是否阻塞」和「是否需要用户线程等待 IO 完成」,同步异步关注全程,阻塞非阻塞关注数据准备阶段。
- BIO 是基础但低效,NIO 多路复用解决了 C10K 问题(主流方案),AIO 理论最优但落地成本高。
- 实际开发无需手写原生 IO 模型,优先选择 Netty 等成熟框架,底层依赖 NIO 多路复用即可满足绝大多数高并发场景。