跳到主要内容

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 对比

维度BIONIOAIO
模型同步阻塞同步非阻塞异步非阻塞
线程模型一连接一线程一线程多连接(Selector)回调/Future
编程复杂度简单复杂复杂
吞吐量
适用场景连接少、短连接连接多、数据轻(聊天)连接多、数据重(文件)
典型框架Netty、Mina不常用
为什么 Netty 用 NIO 而非 AIO?
  1. Linux 的 AIO(io_uring 之前)不够成熟,底层仍是 epoll 模拟
  2. Netty 在 NIO 上做了大量优化(零拷贝、内存池),性能已足够好
  3. NIO 模型更可控,便于优化和调试

常见面试问题

Q1: 同步/异步与阻塞/非阻塞的区别?

答案

  • 同步 vs 异步:关注的是消息通知机制。同步是调用方主动等待结果,异步是被调用方完成后通知调用方
  • 阻塞 vs 非阻塞:关注的是等待时的状态。阻塞是线程挂起等待,非阻塞是立即返回(可能是数据或状态码)
组合说明例子
同步阻塞调用者阻塞等待结果BIO read()
同步非阻塞调用者轮询检查结果NIO channel.read()
异步非阻塞系统完成后回调通知AIO CompletionHandler

Q2: NIO 中 Selector 的底层实现?

答案

Selector 在不同操作系统上使用不同的多路复用机制:

操作系统实现特点
Linuxepoll事件驱动,O(1)O(1) 事件通知
macOSkqueue类似 epoll
Windowsselect/IOCPselect 有 1024 连接限制

Q3: 什么是零拷贝?

答案

传统 IO 读取文件并发送到网络需要 4 次拷贝和 4 次上下文切换。零拷贝减少了不必要的数据拷贝:

  • mmap:将文件映射到用户空间,减少一次内核缓冲区到用户缓冲区的拷贝
  • sendfile:数据在内核空间直接从文件描述符传到 Socket,不经过用户空间

Java 中的零拷贝:

  • FileChannel.transferTo() / transferFrom() —— 底层使用 sendfile
  • MappedByteBuffer(mmap)—— FileChannel.map() 返回

Q4: DirectByteBuffer 和 HeapByteBuffer 的区别?

答案

维度HeapByteBufferDirectByteBuffer
分配位置JVM 堆内存堆外内存(直接内存)
分配方式ByteBuffer.allocate()ByteBuffer.allocateDirect()
创建速度慢(需要系统调用)
IO 性能需要额外拷贝到直接内存直接用于 IO,更快
GC 影响受 GC 管理不受 GC 直接管理
适用场景小数据、频繁分配大数据、长期存活的 IO 缓冲

Netty 的 ByteBuf 在此基础上增加了池化(PooledByteBufAllocator),减少频繁分配/释放的开销。

相关链接