Java IO 模型
Java IO 模型 #
BIO、NIO、AIO 三种IO模型对比,Netty 核心概念,面试高频考点
📋 目录 #
IO 基础概念 #
核心概念 #
| 概念 | 说明 |
|---|---|
| 阻塞IO | 发起IO请求后,线程挂起等待结果,什么都不做 |
| 非阻塞IO | 发起IO请求后,线程继续执行,轮询检查是否就绪 |
| 同步IO | IO操作由应用线程主动完成 |
| 异步IO | IO操作由内核完成,完成后通知应用 |
Unix IO 模型分类 #
IO 执行分两步:
1. 等待数据就绪(数据从网卡到内核缓冲区)
2. 把数据从内核拷贝到用户进程缓冲区
不同模型区别就在这两步的处理方式
BIO 阻塞IO #
模型图 #
代码示例 #
ServerSocket serverSocket = new ServerSocket(8080);
while (!closed) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
try {
byte[] buf = new byte[1024];
int len = socket.getInputStream().read(buf); // 阻塞等待数据
// 处理数据...
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
特点 #
- 优点:实现简单
- 缺点:一个连接一个线程,高并发下线程太多,内存和上下文切换开销大
- 适用场景:连接数少、固定架构,传统阻塞服务
NIO 非阻塞IO #
核心组件 #
Channel (通道)
↓
Buffer (缓冲区)
↓
Selector (选择器)
| 组件 | 作用 |
|---|---|
| Channel | 双向数据通道,代替传统Stream |
| Buffer | 缓冲区,数据读写都经过Buffer |
| Selector | 多路复用器,一个线程监听多个Channel的就绪事件 |
多路复用模型图 #
代码示例 #
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 非阻塞
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while (!closed) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = client.read(buf);
// 处理数据...
}
}
keys.clear();
}
Buffer 读写模式 #
Buffer 属性:
capacity:缓冲区总容量position:当前读写位置limit:读写限制(写模式=capacity,读模式=写入位置)
特点 #
- 一个线程处理多个连接,线程开销小
- 非阻塞,连接没数据不会阻塞线程
- 基于事件驱动,适合高并发场景
- 编程复杂度比BIO高很多
AIO 异步IO #
模型概念 #
- 用户线程发起请求后继续执行,不等待
- 内核完成IO后回调用户线程处理
CompletableFuture或CompletionHandler回调
代码示例 #
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) {
// 处理数据...
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// 处理失败
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理失败
}
});
特点 #
- 真正的异步,内核完成后回调
- 理论上性能更高
- Linux 上 AIO 实现不够完善,实际用得少
- Netty 也没有基于 AIO
三种模型对比 #
| 对比项 | BIO | NIO | AIO |
|---|---|---|---|
| IO方式 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 线程模型 | 一个连接一线程 | 一个线程多连接 | 内核完成回调 |
| 编程复杂度 | 简单 | 复杂 | 更复杂 |
| 并发性能 | 低 | 高 | 理论最高 |
| 实际应用 | 低并发场景 | 主流(Netty) | 很少用 |
适用场景 #
- BIO:连接数少,业务简单,传统Tomcat BIO模式
- NIO:高并发、长连接,Netty、Jetty、Tomcat NIO模式
- AIO:连接数极多,windows平台支持好,Linux实际少用
Netty 核心概念 #
Netty 是什么? #
- 基于 NIO 的高性能网络通信框架
- 封装了JDK NIO 的复杂性,提供易用API
- 解决了TCP粘包拆包问题
- 内置多种编解码,支持HTTP、WebSocket等协议
核心组件 #
| 组件 | 作用 |
|---|---|
| EventLoop | 事件循环线程,处理IO事件 |
| EventLoopGroup | 事件循环线程组 |
| Channel | Netty 对 Socket 的封装 |
| ChannelHandler | 业务处理器,处理入站出站事件 |
| ChannelPipeline | Handler 链,责任链模式 |
| ByteBuf | Netty 缓冲区,替代 NIO ByteBuffer |
线程模型 #
- BossGroup:专门处理accept连接
- WorkerGroup:处理连接的读写事件
- 一个EventLoop线程处理多个Channel
零拷贝 #
Netty 零拷贝优化:
- CompositeByteBuf:组合多个缓冲区,避免数据拷贝
- FileRegion:sendfile 系统调用,直接从内核到网卡,不经过用户空间
- 直接内存:使用堆外内存,避免GC拷贝
面试题汇总 #
基础 #
-
BIO、NIO、AIO 区别?
答案:
维度 BIO NIO AIO IO类型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞 线程模型 一个连接对应一个线程 一个线程处理多个连接 内核完成后回调 编程复杂度 简单 复杂 更复杂 适用场景 连接数少且固定 高并发、长连接 理论性能最高,实际少用 核心差异:
- BIO:accept() 和 read() 都会阻塞,线程只能等,一个连接独占一个线程
- NIO:通过 Selector 多路复用,一个线程可以监听多个 Channel 的就绪事件
- AIO:真正异步,IO操作完全交给内核,完成后回调通知
-
NIO 的 Channel、Buffer、Selector 各自作用?
答案:
组件 作用 Channel 双向数据通道,代替传统的 InputStream/OutputStream。可以读写、支持非阻塞 Buffer 缓冲区,数据读写都要经过 Buffer。本质是一块内存,有 position、limit、capacity 三个关键属性 Selector 多路复用器,一个线程可以监听多个 Channel 的事件(accept、connect、read、write),实现单线程处理多连接 三者关系:
Selector → Channel → Buffer,Selector 注册 Channel,Channel 读写 Buffer -
什么是IO多路复用?
答案:
核心思想: 一个线程可以同时监听多个文件描述符(Socket)的就绪状态,哪个就绪就处理哪个,避免为每个连接开一个线程。
关键点:
- 单线程/少量线程监听多个连接
- 通过
select/poll/epoll等系统调用实现 - 就绪事件驱动,有事件才处理,没事件就阻塞或干别的
类比: 餐厅服务员同时照看多桌客人,哪桌喊"买单"就去哪桌处理,而不是每桌配一个专属服务员
NIO #
-
NIO 为什么能单线程处理多连接?
答案:
两个核心机制:
① 非阻塞 Channel
- Channel 配置为
configureBlocking(false) accept()、read()、write()都立即返回,不会阻塞线程
② Selector 多路复用
- 把多个 Channel 注册到同一个 Selector
- 调用
select()阻塞等待就绪事件 - 事件就绪后,遍历
selectedKeys逐个处理
结果: 单线程可以同时监听几百上千个连接,哪个连接有数据才处理哪个,避免了 BIO 一连接一线程的开销
- Channel 配置为
-
ByteBuffer flip() 方法作用是什么?
答案:
切换 Buffer 从写模式到读模式
写模式: 往 Buffer 写数据
position指向当前写入位置limit=capacity
flip() 做了两件事:
limit = position→ 可读上限设为刚才写入的位置position = 0→ 读指针移到开头
典型用法:
buf.put(data); // 写入数据 buf.flip(); // 切换读模式 channel.read(buf); // 读取 buf.clear(); // 清空复位相关方法:
clear():清空缓冲区,position=0,limit=capacity(写模式)compact():把未读完的数据移到开头,继续写(部分清空)
-
Selector 原理,select/poll/epoll 区别?
答案:
Selector 原理:
- 基于操作系统的多路复用机制(select/poll/epoll)
- 把多个 Channel 注册到 Selector 上
select()调用阻塞等待事件,有事件就绪就返回- 遍历
selectedKeys处理每个就绪事件
select/poll/epoll 对比:
特性 select poll epoll 最大连接数 有限(1024/2048) 无限制 无限制 性能 O(n) 遍历所有fd O(n) 遍历所有fd O(1) 回调就绪 内存拷贝 每次调用拷贝fd集合 每次调用拷贝fd集合 共享内存,无需拷贝 触发方式 水平触发 水平触发 水平/边缘触发 平台 跨平台 跨平台 Linux 专用 水平触发 vs 边缘触发:
- 水平触发 (LT):只要数据就绪就持续通知,直到处理完
- 边缘触发 (ET):只在状态变化时通知一次,必须一次性读完
Netty #
-
Netty 的线程模型?为什么分 BossGroup 和 WorkerGroup?
答案:
Netty 线程模型(主从 Reactor 多线程):
BossGroup (1-N 个 EventLoop) ↓ accept 连接 WorkerGroup (N 个 EventLoop) ↓ 处理读写事件 ChannelPipeline (Handler 链)BossGroup 职责:
- 只负责 accept 新连接
- 接受连接后注册到 WorkerGroup
- 通常 1-2 个线程足够
WorkerGroup 职责:
- 处理已建立连接的所有 IO 事件(read/write)
- 执行 ChannelPipeline 中的业务 Handler
- 线程数通常 = CPU 核心数 × 2
为什么分离?
- 职责单一:accept 是低频操作,read/write 是高频操作
- 性能优化:Boss 只处理连接建立,Worker 专注于业务处理
- 独立伸缩:可以独立配置两组的线程数
-
Netty 如何解决 TCP 粘包拆包问题?
答案:
TCP 粘包拆包原因:
- TCP 是流式协议,没有消息边界
- 发送方可能合并多个小包发送(Nagle 算法)
- 接收方可能一次读取多个包
Netty 内置解决方案:
解码器 原理 适用场景 LineBasedFrameDecoder 按换行符 \n或\r\n分割每行一条消息,如 HTTP、Telnet DelimiterBasedFrameDecoder 按自定义分隔符分割 有特殊分隔符的协议 FixedLengthFrameDecoder 固定长度分割 每条消息长度固定 LengthFieldBasedFrameDecoder 读取消息头中的长度字段 自定义协议(最常用) LengthFieldBasedFrameDecoder 例子:
+--------+----------------+ | Length | Actual Content | | 4 bytes| "HELLO, WORLD" | +--------+----------------+配置:
maxFrameLength=1024, lengthFieldOffset=0, lengthFieldLength=4 -
Netty 的零拷贝实现原理?
答案:
零拷贝(Zero-Copy):减少或避免 CPU 拷贝数据的次数,提升性能
Netty 三种零拷贝方式:
① CompositeByteBuf - 逻辑组合
- 把多个 ByteBuf 组合成一个逻辑视图
- 不需要物理拷贝数据
- 例如:header + body 两个缓冲区,组合成一个处理
② FileRegion - sendfile 系统调用
- 利用 Linux
sendfile()直接把文件从内核缓冲区发送到网卡 - 不经过用户空间,减少一次拷贝
- 适用于文件传输
③ 堆外内存(Direct Buffer)
- ByteBuf 支持直接分配堆外内存
- 避免 Java 堆到 native 堆的拷贝
- 减少 GC 压力
传统 IO vs Netty 零拷贝:
- 传统:
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket → 网卡(4次拷贝) - Netty:
磁盘 → 内核缓冲区 → Socket → 网卡(2次拷贝)
-
Netty 中 ByteBuf 和 JDK ByteBuffer 区别?
答案:
特性 JDK ByteBuffer Netty ByteBuf 读写模式 需要 flip() 切换 自动双指针,无需切换 指针 一个 position readerIndex + writerIndex 扩容 固定大小,需手动 自动扩容,类似 ArrayList 内存类型 heap/direct heap/direct/composite 池化 无 PooledByteBufAllocator 支持池化 API 较简陋 丰富的工具方法 ByteBuf 核心优势:
- 双指针:读用 readerIndex,写用 writerIndex,互不干系,无需 flip
- 自动扩容:写超了自动扩,不用手动 reallocate
- 池化:复用缓冲区,减少 GC 和分配开销
- 组合缓冲区:CompositeByteBuf 可以逻辑组合多个缓冲区
典型代码对比:
// JDK ByteBuffer ByteBuffer buf = ByteBuffer.allocate(1024); buf.put(data); buf.flip(); // 必须手动切换! // Netty ByteBuf ByteBuf buf = Unpooled.buffer(); buf.writeBytes(data); buf.readBytes(out); // 无需 flip!
🔗 相关笔记 #
最后更新: 2026-04-29