IO 面试题: BIO、NIO、AIO 一次说清
IO 模型是 Java 面试的高频题,很多人背了一堆概念,遇到实际场景还是懵。
先搞清楚一个根本问题:IO 等待时,CPU 在干什么?
答案是:干等着。所以要让 CPU 去做别的事。
三种 IO 模型
BIO(同步阻塞)
传统 IO,read 时线程阻塞在那儿等数据:
java
// 服务端:为每个连接分配一个线程
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // 阻塞等连接
new Thread(() -> {
InputStream in = socket.getInputStream();
// read 阻塞:等数据
int data = in.read();
}).start();
}10000 个连接 → 10000 个线程 → 机器直接挂掉。
NIO(同步非阻塞)
一个线程轮询所有连接,有数据了才处理:
java
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) { // 阻塞,但只阻塞 selector
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
}
}
selector.selectedKeys().clear();
}AIO(异步 IO)
操作系统内核完成 IO 后通知你,连轮询都省了:
java
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel ch, Void attachment) {
// 读完了回调这里
ByteBuffer buffer = ByteBuffer.allocate(1024);
ch.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// 处理数据
}
});
}
});核心组件对比
| 组件 | BIO | NIO | AIO |
|---|---|---|---|
| 连接 | Socket | SocketChannel | AsynchronousSocketChannel |
| 数据载体 | Stream | Buffer | ByteBuffer |
| 统一管理 | — | Selector | — |
| 等待方式 | 阻塞 | select 轮询 | 内核回调 |
文件复制:传统 IO vs NIO
java
// 传统 BIO:边读边写,频繁上下文切换
try (FileInputStream in = new FileInputStream("src.txt");
FileOutputStream out = new FileOutputStream("dst.txt")) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
// NIO:transferTo 直接在内核态复制,零拷贝
try (FileChannel in = new FileInputStream("src.txt").getChannel();
FileChannel out = new FileOutputStream("dst.txt").getChannel()) {
long size = in.size();
long transferred = 0;
while (transferred < size) {
transferred += in.transferTo(transferred, size - transferred, out);
}
}选择建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 低并发(<1000) | BIO | 简单够用 |
| 高并发短连接 | NIO | 单线程处理大量连接 |
| 高并发长连接 | NIO / AIO | 减少线程开销 |
| 文件操作 | NIO transferTo | 零拷贝绕过堆外内存 |
常见追问
Q: NIO 为什么比 BIO 性能好?
BIO 的问题是:每个连接一个线程,线程切换成本高。NIO 用一个 selector 线程管理所有连接,只有真正有事件时才处理。
Q: 什么是零拷贝?
传统 IO 要经历:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡。零拷贝跳过用户缓冲区,直接内核之间传输,减少两次 CPU 拷贝。
理解 IO 模型的核心:让 CPU 在等待 IO 时去做别的事,而不是干等着。
