知识模块
☕ Java 知识模块
五、Java IO/NIO
NIO 核心

NIO 核心组件

面试提问

"NIO 的三大核心组件是什么?Buffer 如何工作?"


三大核心组件

NIO(Non-blocking IO)的核心是 Channel、Buffer、Selector 三大组件。

┌─────────────────────────────────────────────────┐
│                  NIO 架构                        │
├─────────────────────────────────────────────────┤
│                                                 │
│    ┌──────────┐    读/写    ┌──────────┐        │
│    │ Channel  │ ←────────→ │  Buffer  │        │
│    │  (通道)   │            │  (缓冲区) │        │
│    └────┬─────┘            └──────────┘        │
│         │                                      │
│         │ 注册事件                              │
│         ↓                                      │
│    ┌──────────┐                                │
│    │ Selector │                                │
│    │(多路复用器)│                                │
│    └──────────┘                                │
│                                                 │
└─────────────────────────────────────────────────┘

Channel(通道)

特点

  • 双向性:可读可写(不同于 InputStream/OutputStream)
  • 非阻塞:可设置为非阻塞模式
  • 与 Buffer 配合:数据必须通过 Buffer 读写

常用 Channel 类型

Channel 类型说明
FileChannel文件通道,阻塞模式
SocketChannelTCP 客户端通道
ServerSocketChannelTCP 服务端通道
DatagramChannelUDP 通道

示例

// 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字符
IntBufferint
LongBufferlong
DoubleBufferdouble

示例

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_ACCEPT16接受连接就绪
OP_CONNECT8连接就绪
OP_READ1读就绪
OP_WRITE4写就绪

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 复制,效率更高

相关题目

  1. ByteBuffer 的 flip() 和 clear() 区别?
  2. 什么情况下用 allocateDirect()?
  3. Selector 如何实现多路复用?
  4. NIO 的零拷贝是如何实现的?

参考资料