BIO、NIO、AIO 区别 ⭐⭐
面试题:说说 BIO、NIO、AIO 的区别
核心回答
BIO、NIO、AIO 是 Java 中三种不同的 I/O 模型,分别代表同步阻塞、同步非阻塞、异步非阻塞 I/O。
三种 I/O 模型对比
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 全称 | Blocking I/O | Non-blocking I/O | Asynchronous I/O |
| JDK 版本 | 1.0 | 1.4 | 1.7 |
| I/O 模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 线程模型 | 一个连接一个线程 | 一个线程处理多个连接 | 回调驱动 |
| 适用场景 | 连接数少 | 连接数多、短连接 | 连接数多、长连接 |
| API 位置 | java.io | java.nio | java.nio.channels |
同步 vs 异步,阻塞 vs 非阻塞
同步/异步:消息通知机制(被调用方是否主动通知)
阻塞/非阻塞:等待结果时的状态(是否挂起)
同步阻塞(BIO):
你去书店买书,问老板有没有《Java并发编程》
老板说:"我查一下",你站着等,直到老板找到书
同步非阻塞(NIO):
你去书店买书,问老板有没有《Java并发编程》
老板说:"我查一下",你去旁边玩手机,过一会儿来问一次
异步非阻塞(AIO):
你去书店买书,问老板有没有《Java并发编程》,留下电话号码
老板说:"找到了给你打电话",你去干别的事
老板找到后打电话通知你
BIO(同步阻塞)
工作原理
Client → Socket → ServerSocket → 新建线程处理
每个连接需要一个独立线程:
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ Client1 │────→│ ServerSocket│────→│ Thread1 │
│ Client2 │────→│ 监听端口 │────→│ Thread2 │
│ Client3 │────→│ │────→│ Thread3 │
└─────────┘ └─────────────┘ └─────────┘
代码示例
// 服务端
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 阻塞等待客户端连接
Socket socket = serverSocket.accept();
// 新建线程处理
new Thread(() -> {
try {
handle(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
private static void handle(Socket socket) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// 阻塞读取数据
String line;
while ((line = reader.readLine()) != null) {
System.out.println("收到: " + line);
}
}
}
缺点
- 线程开销大:每个连接需要一个线程
- 阻塞等待:线程大部分时间处于阻塞状态
- C10K 问题:无法处理大量并发连接
NIO(同步非阻塞)
核心组件
┌─────────────────────────────────────────────────────────┐
│ NIO │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Channel │ │ Buffer │ │ Selector │ │
│ │ (双向通道) │ │ (缓冲区) │ │ (多路复用器) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
工作原理
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ Client1 │────→│ │ │ │
│ Client2 │────→│ Selector │────→│ 单线程处理 │
│ Client3 │────→│ (轮询事件) │ │ │
└─────────┘ └─────────────┘ └─────────────┘
Selector 可以监控多个 Channel 的事件:
- OP_ACCEPT:接受连接
- OP_CONNECT:连接建立
- OP_READ:可读
- OP_WRITE:可写
代码示例
// NIO 服务端
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建 Selector
Selector selector = Selector.open();
// 2. 创建 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
// 3. 注册到 Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 4. 轮询事件(阻塞等待,直到有事件发生)
selector.select();
// 5. 获取就绪的事件
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// 接受新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = client.read(buffer);
if (read > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到: " + new String(data));
}
}
}
}
}
}
零拷贝(Zero-Copy)
传统 I/O:
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡
(4 次拷贝,4 次上下文切换)
NIO 零拷贝(transferTo):
磁盘 → 内核缓冲区 → 网卡
(2 次拷贝,2 次上下文切换)
FileChannel.transferTo(position, count, targetChannel);
AIO(异步非阻塞)
工作原理
Client 发起请求 → 立即返回
↓
操作系统处理 I/O
↓
完成后回调 CompletionHandler
↓
应用程序处理结果
代码示例
// AIO 服务端
public class AIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建异步通道组
AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(
Executors.newFixedThreadPool(4));
// 创建异步服务端 Socket
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open(group);
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, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到: " + new String(data));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 保持主线程运行
Thread.sleep(Long.MAX_VALUE);
}
}
三种模型对比图示
BIO(同步阻塞):
┌──────┐ ┌──────┐ ┌──────┐
│ 用户 │────→│ 内核 │────→│ 设备 │
│ 线程 │←────│ 等待 │←────│ │
└──────┘ └──────┘ └──────┘
全程阻塞
NIO(同步非阻塞):
┌──────┐ ┌──────┐ ┌──────┐
│ 用户 │────→│ 内核 │────→│ 设备 │
│ 线程 │←────│ 就绪 │←────│ │
└──────┘ └──────┘ └──────┘
轮询检查就绪状态
AIO(异步非阻塞):
┌──────┐ ┌──────┐ ┌──────┐
│ 用户 │────→│ 内核 │────→│ 设备 │
│ 线程 │ │ 处理 │ │ │
└──────┘ └──────┘ └──────┘
↓ 回调通知
┌──────────┐
│ 处理结果 │
└──────────┘
使用场景
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 连接数 < 1000 | BIO | 代码简单,性能足够 |
| 连接数 > 1000,短连接 | NIO | 高并发,节省线程 |
| 连接数 > 1000,长连接 | AIO | 异步处理,最高效 |
| 文件传输 | NIO | 零拷贝,高性能 |
| 实时通信 | NIO/AIO | 非阻塞,响应快 |
Netty 的选择
Netty 使用 NIO 而非 AIO,原因:
1. Linux 下 AIO 实现不完善
- Linux AIO 是基于线程池模拟的
- 性能不如 NIO + epoll
2. NIO 已经足够高效
- epoll 可以处理百万级连接
- 代码更成熟稳定
3. 跨平台考虑
- NIO 在各平台表现一致
- AIO 在 Windows 和 Linux 差异大
高频面试题
Q1: NIO 为什么是同步而不是异步?
NIO 的"同步"体现在:
- 用户线程需要主动调用 select() 检查 I/O 状态
- 数据读写需要用户线程自己执行
真正的异步(AIO):
- 用户线程发起 I/O 后立即返回
- 操作系统完成 I/O 后通知用户线程
- 用户线程只需处理结果
Q2: Selector 的 select() 会阻塞吗?
// 会阻塞,直到有就绪的 Channel
selector.select();
// 设置超时时间
selector.select(1000); // 阻塞 1 秒
// 立即返回,不阻塞
selector.selectNow();
Q3: NIO 的 Buffer 为什么要 flip()?
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 写模式(默认)
channel.read(buffer); // 数据写入 buffer
// 切换为读模式
buffer.flip();
// position = 0, limit = 之前 position 的位置
// 读取数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
}
// 清空,重新变为写模式
buffer.clear();
// position = 0, limit = capacity
Q4: 什么是多路复用?
多路复用(Multiplexing):
一个线程同时监控多个 I/O 通道,哪个有数据就处理哪个。
Linux 实现:
- select/poll:遍历所有 fd,O(n)
- epoll:事件驱动,O(1)
Java NIO 在 Linux 下使用 epoll
最佳实践
// 1. NIO 使用 DirectBuffer(堆外内存)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// 优点:减少一次内存拷贝
// 缺点:分配和回收成本高
// 2. 使用线程池处理 NIO 业务逻辑
ExecutorService workerPool = Executors.newFixedThreadPool(4);
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
if (key.isReadable()) {
workerPool.submit(() -> process(key));
}
}
// 3. 注意 ByteBuffer 的复用
// 使用 ThreadLocal 或对象池避免频繁创建
参考链接: