IO 与 NIO
问题
Java 的 BIO、NIO、AIO 有什么区别?NIO 的核心组件是什么?
答案
BIO(Blocking I/O)
传统的同步阻塞 IO,一个连接对应一个线程:
BioServer.java
// BIO 服务端:每个客户端连接都需要一个独立线程处理
ServerSocket server = new ServerSocket(8080);
while (true) {
// accept() 阻塞等待连接
Socket socket = server.accept();
// 为每个连接创建一个线程
new Thread(() -> {
try (InputStream in = socket.getInputStream()) {
byte[] buf = new byte[1024];
int len;
// read() 阻塞等待数据
while ((len = in.read(buf)) != -1) {
System.out.println(new String(buf, 0, len));
}
} catch (IOException e) { e.printStackTrace(); }
}).start();
}
问题:每个连接一个线程,连接多时线程开销巨大。
NIO(Non-blocking I/O)
JDK 1.4 引入的同步非阻塞 IO,一个线程可以处理多个连接:
三大核心组件
| 组件 | 说明 |
|---|---|
| Channel | 双向通道,类似 Stream 但可读可写 |
| Buffer | 数据缓冲区,所有数据通过 Buffer 读写 |
| Selector | 多路复用器,一个线程监听多个 Channel 的事件 |
NioServer.java
// NIO 服务端:单线程通过 Selector 管理多个连接
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册接受连接事件
while (true) {
selector.select(); // 阻塞直到有事件就绪
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.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 len = client.read(buffer);
if (len > 0) {
buffer.flip(); // 切换到读模式
System.out.println(new String(buffer.array(), 0, len));
}
}
}
}
Buffer 的核心概念
Buffer 有三个关键属性:
| 属性 | 说明 |
|---|---|
capacity | 容量,固定不变 |
position | 当前读写位置 |
limit | 可读写的边界 |
BufferDemo.java
ByteBuffer buf = ByteBuffer.allocate(10); // capacity=10, position=0, limit=10
// 写入数据
buf.put((byte) 'H'); // position=1
buf.put((byte) 'i'); // position=2
// 切换到读模式
buf.flip(); // position=0, limit=2
// 读取数据
byte b1 = buf.get(); // 'H', position=1
byte b2 = buf.get(); // 'i', position=2
// 清空(准备下次写入)
buf.clear(); // position=0, limit=capacity
// 或 compact():保留未读数据,将其移到开头
AIO(Asynchronous I/O)
JDK 7 引入的异步非阻塞 IO,通过回调或 Future 通知结果:
AioServer.java(简化)
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open().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 len, ByteBuffer buf) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
}
@Override
public void failed(Throwable exc, ByteBuffer buf) { }
});
}
@Override
public void failed(Throwable exc, Void attachment) { }
});
BIO vs NIO vs AIO 对比
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| 模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 线程模型 | 一连接一线程 | 一线程多连接(Selector) | 回调/Future |
| 编程复杂度 | 简单 | 复杂 | 复杂 |
| 吞吐量 | 低 | 高 | 高 |
| 适用场景 | 连接少、短连接 | 连接多、数据轻(聊天) | 连接多、数据重(文件) |
| 典型框架 | — | Netty、Mina | 不常用 |
为什么 Netty 用 NIO 而非 AIO?
- Linux 的 AIO(io_uring 之前)不够成熟,底层仍是 epoll 模拟
- Netty 在 NIO 上做了大量优化(零拷贝、内存池),性能已足够好
- NIO 模型更可控,便于优化和调试
常见面试问题
Q1: 同步/异步与阻塞/非阻塞的区别?
答案:
- 同步 vs 异步:关注的是消息通知机制。同步是调用方主动等待结果,异步是被调用方完成后通知调用方
- 阻塞 vs 非阻塞:关注的是等待时的状态。阻塞是线程挂起等待,非阻塞是立即返回(可能是数据或状态码)
| 组合 | 说明 | 例子 |
|---|---|---|
| 同步阻塞 | 调用者阻塞等待结果 | BIO read() |
| 同步非阻塞 | 调用者轮询检查结果 | NIO channel.read() |
| 异步非阻塞 | 系统完成后回调通知 | AIO CompletionHandler |
Q2: NIO 中 Selector 的底层实现?
答案:
Selector 在不同操作系统上使用不同的多路复用机制:
| 操作系统 | 实现 | 特点 |
|---|---|---|
| Linux | epoll | 事件驱动, 事件通知 |
| macOS | kqueue | 类似 epoll |
| Windows | select/IOCP | select 有 1024 连接限制 |
Q3: 什么是零拷贝?
答案:
传统 IO 读取文件并发送到网络需要 4 次拷贝和 4 次上下文切换。零拷贝减少了不必要的数据拷贝:
mmap:将文件映射到用户空间,减少一次内核缓冲区到用户缓冲区的拷贝sendfile:数据在内核空间直接从文件描述符传到 Socket,不经过用户空间
Java 中的零拷贝:
FileChannel.transferTo()/transferFrom()—— 底层使用sendfileMappedByteBuffer(mmap)——FileChannel.map()返回
Q4: DirectByteBuffer 和 HeapByteBuffer 的区别?
答案:
| 维度 | HeapByteBuffer | DirectByteBuffer |
|---|---|---|
| 分配位置 | JVM 堆内存 | 堆外内存(直接内存) |
| 分配方式 | ByteBuffer.allocate() | ByteBuffer.allocateDirect() |
| 创建速度 | 快 | 慢(需要系统调用) |
| IO 性能 | 需要额外拷贝到直接内存 | 直接用于 IO,更快 |
| GC 影响 | 受 GC 管理 | 不受 GC 直接管理 |
| 适用场景 | 小数据、频繁分配 | 大数据、长期存活的 IO 缓冲 |
Netty 的 ByteBuf 在此基础上增加了池化(PooledByteBufAllocator),减少频繁分配/释放的开销。