NIO 核心组件
面试提问
"NIO 的三大核心组件是什么?Buffer 如何工作?"
三大核心组件
NIO(Non-blocking IO)的核心是 Channel、Buffer、Selector 三大组件。
┌─────────────────────────────────────────────────┐
│ NIO 架构 │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ 读/写 ┌──────────┐ │
│ │ Channel │ ←────────→ │ Buffer │ │
│ │ (通道) │ │ (缓冲区) │ │
│ └────┬─────┘ └──────────┘ │
│ │ │
│ │ 注册事件 │
│ ↓ │
│ ┌──────────┐ │
│ │ Selector │ │
│ │(多路复用器)│ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────┘Channel(通道)
特点
- 双向性:可读可写(不同于 InputStream/OutputStream)
- 非阻塞:可设置为非阻塞模式
- 与 Buffer 配合:数据必须通过 Buffer 读写
常用 Channel 类型
| Channel 类型 | 说明 |
|---|---|
FileChannel | 文件通道,阻塞模式 |
SocketChannel | TCP 客户端通道 |
ServerSocketChannel | TCP 服务端通道 |
DatagramChannel | UDP 通道 |
示例
// FileChannel 读写文件
RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer); // 读入 buffer
buffer.flip(); // 切换为读模式
while (buffer.hasRemaining()) {
channel.write(buffer); // 从 buffer 写出
}
channel.close();
// SocketChannel 客户端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 非阻塞模式
socketChannel.connect(new InetSocketAddress("localhost", 8080));
while (!socketChannel.finishConnect()) {
// 等待连接完成
}Buffer(缓冲区)
结构
Buffer 本质是一个数组,通过指针管理读写位置:
┌─────────────────────────────────────────────────┐
│ Buffer 结构 │
├─────────────────────────────────────────────────┤
│ │
│ 0 1 2 3 4 5 6 7 │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │ │ │ ✓ │ ✓ │ ✓ │ ✓ │ │ │ │
│ └────┴────┴────┴────┴────┴────┴────┴────┘ │
│ ↑ ↑ │
│ position limit │
│ │
│ capacity = 8 (总容量) │
│ position = 2 (下一个操作位置) │
│ limit = 6 (可操作边界) │
└─────────────────────────────────────────────────┘三个核心属性
| 属性 | 说明 |
|---|---|
capacity | 容量,buffer 大小 |
position | 当前操作位置 |
limit | 可操作边界(写时=capacity,读时=有效数据量) |
Buffer 工作流程
写模式:position 从 0 开始增长,limit = capacity
↓ flip()
读模式:position = 0,limit = 原来的 position
↓ clear() / compact()
重置:恢复写模式常用 Buffer 类型
| Buffer 类型 | 存储数据类型 |
|---|---|
ByteBuffer | 字节 |
CharBuffer | 字符 |
IntBuffer | int |
LongBuffer | long |
DoubleBuffer | double |
示例
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配空间
// 写入数据
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
// 切换为读模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}
// 清空,准备下次写入
buffer.clear();关键方法
| 方法 | 说明 |
|---|---|
allocate(int) | 分配堆内存 buffer |
allocateDirect(int) | 分配直接内存 buffer(效率更高) |
put() | 写入数据 |
get() | 读取数据 |
flip() | 切换写→读模式 |
clear() | 清空 buffer |
compact() | 压缩 buffer(保留未读数据) |
rewind() | 重置 position 为 0(可重复读) |
Selector(多路复用器)
作用
Selector 是 NIO 的核心,一个线程可以管理多个 Channel。
监听的事件类型
| 事件 | 常量 | 说明 |
|---|---|---|
OP_ACCEPT | 16 | 接受连接就绪 |
OP_CONNECT | 8 | 连接就绪 |
OP_READ | 1 | 读就绪 |
OP_WRITE | 4 | 写就绪 |
SelectionKey
当 Channel 注册到 Selector 时,返回 SelectionKey,包含:
- Channel 引用
- Selector 引用
- 感兴趣的事件
- 就绪的事件
- 附加对象(attachment)
示例
// 创建 Selector
Selector selector = Selector.open();
// 创建 ServerSocketChannel 并配置
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 注册到 Selector,监听 ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 事件循环
while (true) {
int readyChannels = selector.select(); // 阻塞直到有事件
if (readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 接受新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close(); // 连接关闭
} else {
buffer.flip();
// 处理数据
}
}
keyIterator.remove(); // 移除已处理的事件
}
}直接内存 vs 堆内存
对比
| 对比项 | 堆内存 Buffer | 直接内存 Buffer |
|---|---|---|
| 分配方式 | allocate() | allocateDirect() |
| 内存位置 | JVM 堆 | 堆外内存 |
| 创建成本 | 低 | 高 |
| IO 效率 | 低(需要复制) | 高(零拷贝) |
| GC 影响 | 受 GC 管理 | 不受 GC 直接管理 |
| 适用场景 | 短生命周期 | 长生命周期、频繁 IO |
原理
堆内存 IO:
JVM 堆 → 复制 → 直接内存 → 内核空间 → 磁盘
直接内存 IO:
直接内存 → 内核空间 → 磁盘(少一次复制)面试要点总结
| 问题 | 答案要点 |
|---|---|
| Channel 特点? | 双向、非阻塞、与 Buffer 配合 |
| Buffer 三属性? | capacity、position、limit |
| flip() 作用? | 切换写→读模式,position=0,limit=原position |
| Selector 作用? | 多路复用,一个线程管理多个 Channel |
| 直接内存优势? | 减少 IO 复制,效率更高 |
相关题目
- ByteBuffer 的 flip() 和 clear() 区别?
- 什么情况下用 allocateDirect()?
- Selector 如何实现多路复用?
- NIO 的零拷贝是如何实现的?
参考资料
- 《Java NIO》- Ron Hitchens
- Java NIO Buffer (opens in a new tab)