Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
IO 模型是 Java 面试的硬核考点。从 BIO 到 NIO 再到 Netty,背后是操作系统 I/O 模型的演进。本文把 IO 的底层逻辑一次讲透。
一、操作系统 I/O 模型基础
在理解 Java IO 之前,先看操作系统的 5 种 I/O 模型:
lua
┌───────────────────────────────────────────────────────────┐
│ 操作系统 I/O 模型对比 │
├───────────────────────────────────────────────────────────┤
│ │
│ 1. 阻塞 I/O (BIO) │
│ 用户线程 ──调用read──▶ 内核 ──等待数据──▶ 内核 ──拷贝数据──▶ 用户 │
│ ↑ 整个过程阻塞 │
│ │
│ 2. 非阻塞 I/O (Non-blocking IO) │
│ 用户线程 ──调用read──▶ 内核(立即返回) │
│ 用户线程 ──轮询read──▶ 内核(数据好了吗?) │
│ 用户线程 ──read──▶ 内核 ──拷贝──▶ 用户 │
│ ↑ read 不阻塞,但要轮询 │
│ │
│ 3. I/O 多路复用 (select/poll/epoll) │
│ 用户线程 ──调用select──▶ 内核(监听多个fd) │
│ 内核: "有fd就绪了!" → 用户线程read │
│ ↑ 一个线程监听多个连接 │
│ │
│ 4. 信号驱动 I/O (SIGIO) │
│ 用户线程 ──注册信号──▶ 内核 │
│ 内核: "数据好了!" (信号) → 用户线程read │
│ │
│ 5. 异步 I/O (AIO) │
│ 用户线程 ──调用aio_read──▶ 内核(立即返回) │
│ 内核: 自己等数据+拷贝 → 完成后通知用户线程 │
│ ↑ 整个过程用户线程不阻塞,内核完成一切 │
│ │
└───────────────────────────────────────────────────────────┘
yaml
关键区别:
BIO: 阻塞 + 单连接单线程
NIO: 非阻塞 + 多路复用(一个线程管理多个连接)
AIO: 异步 + 内核完成所有操作(Callback通知)
NIO vs AIO:
NIO: 用户线程自己轮询/读取 → 同步非阻塞
AIO: 内核完成读取后通知 → 真正异步
二、Java BIO:一连接一线程
java
// BIO 服务端: 每个连接一个线程
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> handle(socket)).start(); // 每个连接一个线程
}
}
private static void handle(Socket socket) {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024];
int len = in.read(buf); // 阻塞等待数据
out.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
css
BIO 瓶颈:
1000 个连接 → 1000 个线程
├── 每个线程约 1MB 栈空间 → 1GB 内存
├── 线程切换开销巨大
├── 大部分线程在 in.read() 上阻塞
└── 无法支撑 C10K (1万并发连接)
┌────────┐ ┌────────┐ ┌────────┐
│ Thread1│ │ Thread2│ │ ThreadN│
│ read()│ │ read()│ │ read()│ ← 大部分时间在阻塞
│ 阻塞... │ │ 阻塞...│ │ 阻塞...│
└────────┘ └────────┘ └────────┘
↑ ↑ ↑
└──────────────┴──────────────┘
│
┌─────┴─────┐
│ Selector │ ← NIO: 一个线程管理所有连接
└───────────┘
BIO 线程池优化
java
// 线程池版 BIO (缓解但不能解决)
public class BioPoolServer {
private static final ExecutorService pool =
Executors.newFixedThreadPool(200);
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
pool.submit(() -> handle(socket));
}
}
}
// 最多 200 个并发连接,第 201 个连接排队等待
// 仍然无法突破 C10K
三、Java NIO:Buffer + Channel + Selector
1. Buffer
java
// Buffer 是 NIO 的数据容器
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Buffer 三个核心指针:
// position: 当前读写位置
// limit: 读写限制
// capacity: 总容量
// 写模式:
buffer.put((byte) 'H');
buffer.put((byte) 'i');
// position=2, limit=1024, capacity=1024
buffer.flip(); // 切换到读模式
// position=0, limit=2, capacity=1024
// 读模式:
byte b = buffer.get(); // 'H'
// position=1, limit=2, capacity=1024
buffer.clear(); // 清空,准备再写
// position=0, limit=1024, capacity=1024
ini
Buffer 状态转换:
写模式: flip() 读模式:
┌───────────────────┐ ┌───────────────────┐
│ capacity=1024 │ │ capacity=1024 │
│ limit=1024 │ ─────────────▶ │ limit=2 │
│ position=2 │ │ position=0 │
│ ┌─┬─┬─┬─┬─┬─┬─┐ │ │ ┌─┬─┬─┬─┬─┬─┬─┐ │
│ │H│i│ │ │ │ │ │ │ │ │H│i│ │ │ │ │ │ │
│ └─┴─┴─┴─┴─┴─┴─┘ │ │ └─┴─┴─┴─┴─┴─┴─┘ │
│ ↑ │ │ ↑ │
│ position │ │ position │
└───────────────────┘ └───────────────────┘
2. Channel
java
// Channel 是双向数据通道
// 可读可写,支持非阻塞模式
// 文件 Channel
FileChannel fc = new FileInputStream("file.txt").getChannel();
// 网络 Channel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false); // 非阻塞模式
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 8080));
3. Selector(核心)
java
// Selector 是 NIO 的核心: 一个线程管理多个 Channel
public class NioServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT); // 注册 Accept 事件
while (true) {
selector.select(); // 阻塞等待事件就绪
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = sc.read(buf);
if (len > 0) {
buf.flip();
sc.write(buf);
} else if (len == -1) {
sc.close();
}
}
}
}
}
}
csharp
Selector 工作原理:
┌───────────────────────────────────────────────────┐
│ 单线程 + Selector │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Selector │ │
│ │ ┌─────────┬─────────┬─────────┐ │ │
│ │ │ OP_ACCEPT│ OP_READ │ OP_WRITE│ ... │ │
│ │ └────┬────┴────┬────┴────┬────┘ │ │
│ └───────┼─────────┼─────────┼────────────────┘ │
│ │ │ │ │
│ ┌─────▼───┐ ┌───▼───┐ ┌───▼───┐ │
│ │Channel 1│ │Chann 2│ │Chann N│ ... │
│ │(连接1) │ │(连接2)│ │(连接N)│ │
│ └─────────┘ └───────┘ └───────┘ │
│ │
│ selector.select() 返回就绪的 Channel 数量 │
│ 只处理有数据的 Channel,不轮询空闲连接 │
│ │
│ 1 个线程 → 管理 10000+ 连接 │
└───────────────────────────────────────────────────┘
4. epoll vs select/poll
css
底层数据结构差异:
select/poll (Java NIO 在 Linux 上的默认实现):
┌────────────────────────────────┐
│ 每次调用: │
│ 1. 把所有 fd 拷贝到内核 │ ← O(n) 拷贝
│ 2. 遍历所有 fd 检查就绪 │ ← O(n) 遍历
│ 3. 返回就绪 fd 数量 │
│ fd 数量限制: select=1024 │
└────────────────────────────────┘
epoll (Java NIO 在 Linux 上可配置使用):
┌────────────────────────────────┐
│ 事件驱动: │
│ 1. 注册 fd 到 epoll 实例 │ ← O(1) 注册
│ 2. 内核通过回调通知就绪 fd │ ← O(1) 通知
│ 3. 只返回就绪 fd 列表 │ ← O(就绪数)
│ fd 数量无限制 │
└────────────────────────────────┘
性能差异:
10000 连接, 10 个活跃:
select: 遍历 10000 个 fd
epoll: 只处理 10 个就绪 fd
四、Java AIO:异步 I/O
java
// AIO (JDK 7+): 真正的异步 I/O
// 内核完成数据读取后回调通知
public class AioServer {
public static void main(String[] args) throws IOException {
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 buf = ByteBuffer.allocate(1024);
client.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer len, ByteBuffer buf) {
buf.flip();
client.write(buf);
}
@Override
public void failed(Throwable exc, ByteBuffer buf) { }
});
}
@Override
public void failed(Throwable exc, Void attachment) { }
});
// 主线程不阻塞,AIO 由内核+线程池完成
Thread.currentThread().join();
}
}
yaml
AIO 实现差异:
Linux: epoll 模拟异步 (不是真正的 AIO)
Windows: IOCP (真正的异步 I/O)
→ Java AIO 在 Linux 上性能不如 NIO
→ 这就是为什么 Netty 选择了 NIO 而不是 AIO
→ Netty 官方曾尝试支持 AIO 但最终放弃
五、三种 IO 对比
| 对比维度 | BIO | NIO | AIO |
|---|---|---|---|
| 通信模型 | 阻塞 | 非阻塞 | 异步 |
| 连接管理 | 一连接一线程 | 一线程管多连接 | 回调驱动 |
| 数据流 | 单向流 | 双向 Buffer | 双向 Buffer |
| 并发能力 | 低(<1000) | 高(>10000) | 高(>10000) |
| 编程复杂度 | 简单 | 复杂 | 中等 |
| Linux 底层 | 阻塞 I/O | epoll | epoll 模拟 |
| 代表框架 | Tomcat BIO | Netty/Mina | JDK AIO |
六、零拷贝
零拷贝是减少数据在内核态和用户态之间拷贝次数的技术:
markdown
传统数据传输 (4次拷贝 + 4次上下文切换):
磁盘 ──DMA拷贝──▶ 内核缓冲区 ──CPU拷贝──▶ 用户缓冲区
↓ CPU拷贝
Socket缓冲区 ──DMA拷贝──▶ 网卡 ←──────┘
1. DMA拷贝: 磁盘→内核读缓冲区 (0次上下文切换)
2. CPU拷贝: 内核读缓冲区→用户缓冲区 (切换到用户态)
3. CPU拷贝: 用户缓冲区→Socket缓冲区 (切换到内核态)
4. DMA拷贝: Socket缓冲区→网卡 (切换回来)
mmap (3次拷贝 + 4次切换):
磁盘 ──DMA拷贝──▶ 内核缓冲区
↓ 内存映射(0拷贝)
用户空间映射 ──────────┘
↓ CPU拷贝
Socket缓冲区 ──DMA拷贝──▶ 网卡
sendfile (2次拷贝 + 2次切换):
磁盘 ──DMA拷贝──▶ 内核缓冲区 ──DMA拷贝──▶ 网卡
(完全绕过用户空间,0次CPU拷贝)
Linux 2.4+ sendfile: SG-DMA
磁盘 ──DMA拷贝──▶ 内核缓冲区 ──DMA─SG─▶ 网卡
(真正的零拷贝: 0次CPU拷贝)
Java 中的零拷贝
java
// 1. MappedByteBuffer (mmap)
FileChannel fc = new RandomAccessFile("file.txt", "r").getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
// 文件直接映射到用户空间内存,无需拷贝
// 2. FileChannel.transferTo (sendfile)
FileChannel in = new FileInputStream("file.txt").getChannel();
FileChannel out = new FileOutputStream("copy.txt").getChannel();
in.transferTo(0, in.size(), out);
// 内核直接从源文件拷贝到目标文件,绕过用户空间
// 3. Netty 中的零拷贝
// - CompositeByteBuf: 多个 Buffer 合并,无需拷贝
// - Unpooled.wrappedBuffer(): 包装数组,不拷贝
// - FileRegion: 底层使用 transferTo
ByteBuf header = Unpooled.buffer(10);
ByteBuf body = Unpooled.buffer(90);
ByteBuf all = Unpooled.wrappedBuffer(header, body); // 零拷贝合并
yaml
零拷贝技术对比:
传统: 磁盘→内核→用户→Socket→网卡 (4次拷贝)
mmap: 磁盘→内核→(映射)→Socket→网卡 (3次拷贝)
sendfile: 磁盘→内核→网卡 (2次拷贝)
SG-DMA: 磁盘→内核→网卡 (0次CPU拷贝, 真正零拷贝)
七、Netty:NIO 的工程化封装
java
// Netty 服务端
public class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // Accept
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Read/Write
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new StringDecoder())
.addLast(new StringEncoder())
.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到: " + msg);
ctx.writeAndFlush("echo: " + msg);
}
});
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
}
}
vbnet
Netty Reactor 模型:
┌───────────────────────────────────────────────┐
│ Main Reactor (BossGroup) │
│ ┌─────────────┐ │
│ │ Acceptor │ │
│ └──────┬──────┘ │
│ │ 分发连接 │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Sub │ │Sub │ │Sub │ │
│ │Reactor│ │Reactor│ │Reactor│ │
│ │(Worker)│ │(Worker)│ │(Worker)│ │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Pipeline Pipeline Pipeline │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Decoder │ │Decoder │ │Decoder │ │
│ │Handler │ │Handler │ │Handler │ │
│ │Encoder │ │Encoder │ │Encoder │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────┘
Boss: 接受连接 → 分配给 Worker
Worker: 处理 I/O → 走 Pipeline → 业务 Handler
Netty 核心设计
markdown
Netty 为什么快:
1. Reactor 线程模型 → 避免锁竞争
2. 零拷贝 → CompositeByteBuf/FileRegion
3. 无锁化串行设计 → 同一 Channel 的所有操作在同一个线程
4. ByteBuf 池化 → 减少 GC 压力
5. 高性能序列化 → 内置多种编解码器
6. 可扩展的 Pipeline → 责任链模式
主线程 Reactor 模型 (Netty 3):
单线程处理 Accept + Read/Write
从线程 Reactor 模型 (Netty 4):
Boss 线程: Accept
Worker 线程: Read/Write + 业务处理
同一 Channel 绑定同一 EventLoop → 无锁串行
八、常见面试追问
Q1:NIO 一定比 BIO 快吗?
不一定。在连接数少且活跃的场景,BIO 的简单实现反而更快(没有 Selector 的开销)。NIO 的优势在于连接数多但活跃度低(如 IM 长连接),一个线程管理多个连接。
Q2:select、poll、epoll 的区别?
- select: fd 数组有 1024 限制,每次调用需拷贝全部 fd,O(n) 遍历
- poll: 无 fd 数量限制,但仍然 O(n) 遍历
- epoll: 事件驱动,只返回就绪 fd,O(1) 效率,fd 无限制
Q3:Netty 为什么不使用 AIO?
- Linux AIO 不成熟,底层仍用 epoll 模拟,没有性能优势
- AIO 的回调模型编程复杂
- Netty 在 NIO 上已充分优化,性能足够好
- Netty 5 曾尝试支持 AIO,但因性能收益不大最终放弃
Q4:ByteBuf 和 ByteBuffer 的区别?
- ByteBuffer: 固定长度,position/limit 状态管理复杂,不动态扩容
- ByteBuf: 动态扩容,读写指针独立(不需要 flip),支持池化,支持零拷贝(CompositeByteBuf)
九、总结
bash
IO 演进链路:
BIO (阻塞, 一连接一线程)
↓ 突破 C10K
NIO (非阻塞 + 多路复用, 一线程管多连接)
↓ 工程化封装
Netty (Reactor + Pipeline + 零拷贝 + 池化)
↓ 异步化
AIO (内核回调, Linux 上用 epoll 模拟)
底层核心: epoll (I/O 多路复用) + sendfile/mmap (零拷贝)
Java 核心: Channel + Buffer + Selector
Netty 核心: EventLoop + Pipeline + ByteBuf
理解 IO 模型的演进,核心是理解操作系统层面的 I/O 模型差异。Java 的 IO API 只是对操作系统接口的封装,真正决定性能的是 epoll 和零拷贝等底层技术。