Java面试高频考点-IO与NIO-整合篇
本文摘要: 本文系统介绍了Java IO/NIO相关核心概念与技术要点,主要包括三部分内容: BIO/NIO/AIO对比:详细解析了三种IO模型的本质区别,BIO采用阻塞式线程模型,NIO基于Selector实现多路复用,AIO则是异步回调机制。重点分析了各自的适用场景和优缺点。 零拷贝技术:深入剖析了传统IO的多次拷贝问题,介绍了sendfile和mmap两种零拷贝实现方式,以及在Java中的具
05-IO与NIO
前言
本文件汇总专题「05-IO与NIO」,收录该目录下的所有 Markdown 原文,并提供可点击目录便于跳转查阅。
目录
- Java面试题合集-42-BIO-NIO-AIO区别.md
- 1)一句话结论
- 2)先把三个概念分开:同步/异步 vs 阻塞/非阻塞
- 3)BIO:一个连接一个线程(简单但不抗连接数)
- 4)NIO:一个线程看很多连接(Selector 多路复用)
- 5)AIO:我去忙,你做完叫我(回调/CompletionHandler)
- 6)选型建议(面试可直接给)
- 7)一句话收尾(面试可直接用)
- Java面试题合集-43-零拷贝是什么.md
- 1)一句话结论
- 2)先看“传统拷贝”到底拷了几次
- 3)sendfile:把“用户态那一段搬运”省掉
- 4)mmap:把文件映射到内存(减少拷贝但要注意资源)
- 5)Netty 里的“零拷贝”更多指“减少用户态内存复制”
- 6)一句话收尾(面试可直接用)
- Java面试题合集-44-Netty线程模型.md
- 1)一句话结论
- 2)先用一张图记住 Boss/Worker
- 3)EventLoop:为什么“单线程串行”反而更稳?
- 4)Pipeline:把网络处理拆成可插拔的“流水线”
- 5)为什么性能好?(面试说 4 点就够)
- 6)面试追问:业务 handler 里能不能做耗时操作?
- 7)一句话收尾(面试可直接用)
- Java面试题合集-45-Selector原理.md
- 1)一句话结论
- 2)Selector 的核心对象关系
- 3)最小使用流程(面试讲步骤最关键)
- 4)为什么 Selector 能提升并发?
- 5)面试加分:OP_WRITE 为什么容易踩坑?
- 6)一句话收尾(面试可直接用)
- Java面试题合集-46-序列化方案怎么选.md
- 1)一句话结论
- 2)三类方案对比表(面试直接用)
- 3)JSON:通用但别滥用
- 4)Protobuf:小、快、可演进
- 5)JDK 原生序列化:为什么生产不推荐?
- 6)怎么选?给一个“按场景”的决策树
- 7)一句话收尾(面试可直接用)
Java面试题合集-42-BIO-NIO-AIO区别.md
BIO / NIO / AIO 的区别与适用场景?
这题如果只背一句“BIO 阻塞、NIO 非阻塞、AIO 异步”,面试官通常会追问:
你说的“阻塞/非阻塞/异步”到底阻塞在哪里?谁在等谁?
NIO 为什么还要 Selector?它真的是“完全不阻塞”吗?
这篇用“人话 + 图”把三者关系讲清楚。
1)一句话结论
BIO 是线程阻塞等待 IO;NIO 是基于 Selector 的 IO 多路复用,单线程可管理多连接,读写调用仍可能发生“数据未就绪”的非阻塞返回;AIO 是真正的异步 IO(由系统完成 IO 后回调通知),应用线程不需要轮询等待。选型看连接数、并发模型、延迟/吞吐目标与平台支持。
2)先把三个概念分开:同步/异步 vs 阻塞/非阻塞
很多人把它们混在一起。
- 阻塞/非阻塞:调用
read/write时,线程会不会卡住等待? - 同步/异步:IO 的完成通知方式是谁来“确认完成”?
- 同步:你自己去问(调用返回时你知道结果)
- 异步:别人做完告诉你(回调/事件通知)
所以常见组合是:
- BIO:同步 + 阻塞
- NIO:同步 + 非阻塞(配合多路复用)
- AIO:异步(通常表现为非阻塞)
3)BIO:一个连接一个线程(简单但不抗连接数)
Client1 ──> [Thread-1] 阻塞 read
Client2 ──> [Thread-2] 阻塞 read
Client3 ──> [Thread-3] 阻塞 read
...
优点:
- 代码简单直观
- 适合连接数少、并发不高的场景
缺点:
- 连接数上来线程数爆炸:线程栈内存、上下文切换成本很高
- 大量线程阻塞等待,资源利用率差
适用:
- 内部工具、低并发服务、连接数很少的场景
4)NIO:一个线程看很多连接(Selector 多路复用)
NIO 的关键不是“完全不阻塞”,而是:
让一个线程同时管理很多连接,只在“有事件发生”的时候再处理。
┌───────────────┐
Channel1 ──►│ │
Channel2 ──►│ Selector │──► 事件就绪后再处理
Channel3 ──►│ │
└───────────────┘
优点:
- 少量线程支撑大量连接(高并发连接)
- 适合高连接数、短任务处理的网络服务
注意点(面试加分):
- NIO 的 read/write 是“非阻塞调用”,可能读到 0 字节/写不完,需要配合状态机与缓冲区处理
- 复杂度高于 BIO:半包/粘包、事件循环、Buffer 管理
适用:
- 高连接数(长连接)场景:IM、网关、推送
- 需要更高吞吐/更少线程的网络服务
5)AIO:我去忙,你做完叫我(回调/CompletionHandler)
应用线程:提交 IO 请求 ──► OS/内核执行 IO ──► 完成后回调通知
优点:
- 线程不需要轮询就绪事件
- 理论上更“异步友好”
现实提醒(更像工程师的回答):
- AIO 的收益依赖操作系统与实现质量;在 Java 生态中网络编程主流仍是 NIO + Netty
适用:
- 需要异步模型且平台支持良好的场景
6)选型建议(面试可直接给)
连接数少 + 简单优先:BIO(或线程池 + BIO)
连接数多 + 高并发网络:NIO(生产常用 Netty)
异步回调模型 + 平台支持好:AIO(了解即可)
7)一句话收尾(面试可直接用)
BIO 以线程阻塞等待 IO,简单但线程成本高;NIO 用 Selector 做多路复用,少量线程管理大量连接,但需要处理非阻塞读写与状态机;AIO 是异步完成通知模型,线程不等待,由系统完成后回调。生产网络服务通常以 NIO/Netty 为主。
Java面试题合集-43-零拷贝是什么.md
什么是零拷贝?在 Java/Netty 中如何体现?
零拷贝(Zero-Copy)听起来像“完全不拷贝”,其实更准确的理解是:
尽量减少不必要的内存拷贝与上下文切换,让数据更少地在“用户态/内核态/缓冲区”之间来回搬家。
1)一句话结论
零拷贝是通过 DMA、内核缓冲区复用、sendfile、mmap 等机制减少用户态与内核态之间的数据拷贝次数和上下文切换;在 Java/Netty 中常见体现包括
FileChannel.transferTo/transferFrom(sendfile 路径)、DirectBuffer(减少一次拷贝)、以及 ByteBuf 的 slice/retain(减少数据复制)。
2)先看“传统拷贝”到底拷了几次
以“读文件 → 发到网络”为例(简化示意):
磁盘 --DMA--> 内核缓冲区
内核缓冲区 --copy--> 用户缓冲区(read)
用户缓冲区 --copy--> 内核 socket 缓冲区(write)
内核 socket 缓冲区 --DMA--> 网卡
你会看到至少两次“内核<->用户”的 copy(加上 DMA),这就是传统路径的成本来源。
3)sendfile:把“用户态那一段搬运”省掉
sendfile(概念示意):
磁盘 --DMA--> 内核缓冲区
内核缓冲区 ---------> 内核 socket 缓冲区(内核内转移/引用)
内核 socket 缓冲区 --DMA--> 网卡
应用线程不再需要把数据 read 到用户态再 write 回内核态。
Java NIO 里常见入口:
FileChannel.transferTo(...)FileChannel.transferFrom(...)
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class TransferToDemo {
static void sendFile(SocketChannel sc, String path) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile(path, "r");
FileChannel fc = raf.getChannel()) {
long pos = 0;
long size = fc.size();
while (pos < size) {
pos += fc.transferTo(pos, size - pos, sc);
}
}
}
}
4)mmap:把文件映射到内存(减少拷贝但要注意资源)
mmap 的思路是把文件映射到进程地址空间,减少显式 copy,但它不是“免费的午餐”:
- 映射/页面回收有成本
- 大量 mmap 可能带来地址空间压力
- 需要注意释放时机(Java 场景有历史坑)
面试里能说到“mmap 也是零拷贝思路之一,但要谨慎使用”就很稳。
5)Netty 里的“零拷贝”更多指“减少用户态内存复制”
Netty 常见的零拷贝手段(面试加分点):
5.1 ByteBuf 的 slice/duplicate(共享底层内存)
// 伪代码:slice 产生视图,不复制数据
ByteBuf buf = ...;
ByteBuf header = buf.slice(0, 10);
ByteBuf body = buf.slice(10, buf.readableBytes() - 10);
这类操作减少了“new byte[] 再拷贝”的成本。
5.2 CompositeByteBuf(逻辑合并,多段拼装)
把多个 ByteBuf 组合成一个“逻辑上的连续缓冲”,避免拷贝拼接。
5.3 DirectBuffer(直接内存)
DirectBuffer 能减少一次“JVM 堆 → 内核”的拷贝(具体取决于实现),但要注意:
- 直接内存也是内存,过量会 OOM(Direct buffer memory)
6)一句话收尾(面试可直接用)
零拷贝的目标是减少内存拷贝和上下文切换;Java 里
FileChannel.transferTo常走 sendfile 路径,Netty 里通过 ByteBuf 的 slice/CompositeByteBuf/DirectBuffer 等减少数据复制与搬运。它提升吞吐与降低延迟,但仍需关注平台支持与内存管理成本。
Java面试题合集-44-Netty线程模型.md
Netty 线程模型是什么?为什么性能好?
Netty 的性能好,很多时候不是“它写得更快”,而是:
它把网络 IO 这类高并发场景的线程与事件组织方式做对了。
1)一句话结论
Netty 基于 Reactor 模型:Boss 线程负责 accept,新连接注册到 Worker(EventLoopGroup),Worker 的 EventLoop 负责该连接的读写事件与 Pipeline 回调;EventLoop 单线程串行处理同一 Channel 的事件,减少锁竞争,同时利用 NIO 的 Selector 少线程管理多连接,因此吞吐高、延迟更稳定。
2)先用一张图记住 Boss/Worker
┌──────────────┐
Client -> │ BossGroup │ 负责 accept
└──────┬───────┘
│ 注册新连接到某个 Worker
┌──────▼──────────────────────────┐
│ WorkerGroup │
│ EventLoop1 EventLoop2 ... │
└──────┬──────────┬───────────────┘
│ │
ChannelA ChannelB (一个 Channel 绑定一个 EventLoop)
核心点:
- Boss:接新客(accept)
- Worker:服务已入座的客(read/write)
3)EventLoop:为什么“单线程串行”反而更稳?
Netty 的关键设计之一是:
同一个 Channel 的 IO 事件与 handler 回调由同一个 EventLoop 串行执行。
收益:
- 少锁:同一连接内的业务处理通常不需要到处加锁
- 少切换:事件循环模型减少线程上下文切换
注意:它不是“全系统只有一个线程”,而是“每个 EventLoop 单线程串行处理其负责的一批连接”。
4)Pipeline:把网络处理拆成可插拔的“流水线”
Netty 处理链通常是:
ByteBuf 入站 -> decoder -> handler -> encoder -> ByteBuf 出站
工程收益:
- 每个 handler 只做一件事(解码/鉴权/业务/编码)
- 易复用、易插拔(非常适合做网关、协议栈)
5)为什么性能好?(面试说 4 点就够)
1)NIO + Selector:少线程支撑多连接
2)Reactor 模型:事件驱动,避免每连接一线程
3)EventLoop 串行:减少锁竞争与线程切换
4)ByteBuf 与内存池:减少内存分配与拷贝(高频收发时收益明显)
6)面试追问:业务 handler 里能不能做耗时操作?
能,但不建议直接在 IO 线程(EventLoop)里做长时间阻塞:
- 会阻塞该 EventLoop 负责的所有连接的 IO 事件
- 导致延迟飙升
正确做法:
- 把耗时任务丢到业务线程池(异步执行)
- IO 线程只做快速处理与派发
7)一句话收尾(面试可直接用)
Netty 采用 Reactor 线程模型:Boss accept、Worker 负责读写与 Pipeline 回调;EventLoop 单线程串行处理同一连接减少锁竞争,配合 Selector 少线程支持多连接,并通过 ByteBuf/内存池降低拷贝与分配开销,因此性能与延迟表现优秀;耗时业务应异步转移到业务线程池避免阻塞 EventLoop。
Java面试题合集-45-Selector原理.md
Selector 的工作原理是什么?
如果你把 NIO 想成“一个客服同时看很多聊天窗口”,那 Selector 就是客服旁边的“消息提醒面板”:
哪个窗口来消息了、哪个窗口能发送了、哪个窗口断开了——它统一告诉你,你再去处理。
1)一句话结论
Selector是 Java NIO 的多路复用器,允许一个线程监听多个 Channel 的事件(OP_ACCEPT/OP_READ/OP_WRITE 等);底层通常依赖操作系统的 select/poll/epoll/kqueue 等机制。应用通过select()阻塞等待就绪事件,然后遍历 selectedKeys 处理对应 Channel,从而实现少线程支撑高并发连接。
2)Selector 的核心对象关系
Selector
├─ SelectionKey( channelA, interestOps=READ )
├─ SelectionKey( channelB, interestOps=READ|WRITE )
└─ ...
你在注册时告诉 Selector:
- 我关心哪些事件(interestOps)
Selector 在 select 之后告诉你:
- 哪些 Channel 就绪了(selectedKeys)
3)最小使用流程(面试讲步骤最关键)
1)open Selector
2)Channel 配置为 non-blocking
3)channel.register(selector, OP_READ/OP_ACCEPT/...)
4)while(true):
selector.select()
遍历 selectedKeys
根据 key.isReadable/isWritable/isAcceptable 处理
移除 key(避免重复处理)
示意代码(精简版):
while (true) {
selector.select(); // 阻塞等待就绪事件
var it = selector.selectedKeys().iterator();
while (it.hasNext()) {
var key = it.next();
it.remove();
if (key.isReadable()) {
// read...
}
}
}
4)为什么 Selector 能提升并发?
BIO 模型中,一个连接一个线程,线程大量阻塞等待。
Selector 模型中,一个线程“统一等待多个连接的事件”,就绪才处理:
线程不需要为“没有数据的连接”干等
所以它更适合:
- 大量长连接
- 事件驱动的网络服务(IM/网关/推送)
5)面试加分:OP_WRITE 为什么容易踩坑?
很多人以为“要写就注册 OP_WRITE”,但实际:
- Socket 缓冲区很多时候是可写的,OP_WRITE 可能持续触发
- 如果你一直对 OP_WRITE 感兴趣,会造成事件循环空转(CPU 飙高)
正确思路:
- 只有当你“真的写不完,需要等缓冲区可写”时才关注 OP_WRITE
- 写完就取消 OP_WRITE 关注
6)一句话收尾(面试可直接用)
Selector 通过 OS 多路复用让单线程监听多个 Channel 事件:注册 interestOps,select 阻塞等待就绪,遍历 selectedKeys 处理;它减少了每连接一线程的阻塞等待,适合高连接数长连接场景。注意 OP_WRITE 需要按需注册,避免空转。
Java面试题合集-46-序列化方案怎么选.md
常见序列化方案怎么选?(JDK/JSON/Protobuf)
序列化这题面试官最想听你说清楚三件事:
1)你要给谁用(前端/服务间/落库)
2)你最在意什么(可读性/性能/兼容性)
3)你能不能避免“反序列化安全坑”
1)一句话结论
选择序列化方案要权衡可读性、体积、性能、跨语言与兼容性:JSON 可读性好、跨语言强但体积大且解析成本高;Protobuf 体积小、性能好、跨语言且有 schema,但不直观;JDK 原生序列化使用方便但性能与安全性口碑差,生产中通常不推荐对外使用。服务间通信常用 Protobuf/JSON,落库更看可演进性与检索需求。
2)三类方案对比表(面试直接用)
| 方案 | 可读性 | 体积 | 性能 | 跨语言 | 兼容性 | 常见场景 |
|---|---|---|---|---|---|---|
| JDK 序列化 | 差 | 中/大 | 一般 | 弱 | 一般 | Java 内部临时(不推荐对外) |
| JSON | 很好 | 大 | 一般 | 很强 | 较好(字段可选) | 前后端、开放接口、日志 |
| Protobuf | 差 | 小 | 好 | 很强 | 很好(schema 演进) | RPC/微服务内部通信 |
3)JSON:通用但别滥用
优点:
- 人能读、能调试
- 前后端天然友好
缺点:
- 体积大,解析耗 CPU
- 类型信息弱(需要约定字段)
工程建议:
- 对外接口用 JSON 合适
- 内部高频 RPC 可考虑 Protobuf
4)Protobuf:小、快、可演进
优点:
- 二进制体积小,序列化/反序列化快
- Schema 明确,字段可选/新增字段对兼容友好
- 跨语言成熟
缺点:
- 不直观,不适合直接当日志
- 需要维护 .proto(但这通常是优势)
面试加分点:
Protobuf 适合服务间高频通信与带宽敏感场景;字段演进要遵循“不复用 tag、不随便改类型”的规则。
5)JDK 原生序列化:为什么生产不推荐?
常见原因(说 2 个就够):
- 性能与体积表现一般
- 历史上反序列化安全问题多(对不可信输入极其危险)
稳妥说法:
JDK 序列化不要用于接收不可信数据;生产更倾向使用 JSON/Protobuf,并做白名单/类型限制等防护。
6)怎么选?给一个“按场景”的决策树
要给前端/开放接口? -> JSON
服务间高频通信/带宽敏感? -> Protobuf
只在本进程内缓存/临时? -> 看场景(一般也能用 JSON/自定义结构),避免 JDK 序列化对外暴露
7)一句话收尾(面试可直接用)
JSON 胜在可读与生态,Protobuf 胜在体积/性能/可演进,JDK 序列化不建议对外使用且要警惕反序列化安全;选型按使用方(对外/对内)、性能与带宽目标、以及兼容演进需求综合决定。
更多推荐


所有评论(0)