写业务代码时,你可能天天都在和 IO 打交道:读取文件 、调用 HTTP 接口 、操作数据库 、读写 Redis 。但有没有想过:这些 IO 操作底层是怎么工作的 ?为什么文件读取会阻塞 ?NIO 到底好在哪 ?为什么 Netty 用 NIO 而不用 BIO?
很多同学觉得 IO 是「底层」东西,交给框架就行了。但理解 IO 模型,能帮你:
- 选型时知道什么时候该用同步、什么时候该用异步
- 排查问题时知道瓶颈在 CPU、内存、网络还是 IO
- 理解 Netty、Redis、MQ 等中间件的底层原理
这篇帮你把 IO/NIO 彻底搞明白。下面我按「IO 基础 → IO 模型演进 → NIO 核心组件 → 实战场景」的顺序往下聊。
1. IO 基础:什么是输入输出?📡
1.1 什么是 IO?
IO(Input/Output) 就是数据的输入 和输出。在计算机里,一切 IO 都是数据的流动:
text
数据流动方向:
│
├─ 输入(Input):数据从外部设备流向程序
│ ├─ 读取文件
│ ├─ 接收网络数据
│ └─ 读取键盘输入
│
└─ 输出(Output):数据从程序流向外部设备
├─ 写入文件
├─ 发送网络数据
└─ 屏幕输出
1.2 常见的 IO 设备
| 设备 | 方向 | 特点 |
|---|---|---|
| 磁盘 | 读写 | 相对较慢,顺序读写快 |
| 网络 | 收发 | 受网络延迟影响 |
| 内存 | 读写 | 极快 |
| 键盘/屏幕 | 输入/输出 | 交互用 |
1.3 Java 中的 IO 分类
java
// 字节流
InputStream / OutputStream
// 字符流(带编码转换)
Reader / Writer
// 节点流(直接操作数据源)
FileInputStream / FileOutputStream
SocketInputStream / SocketOutputStream
// 处理流(包装其他流)
BufferedInputStream / BufferedReader
DataInputStream / DataOutputStream
ObjectInputStream / ObjectOutputStream
2. IO 模型演进:从阻塞到异步 🚀
2.1 阻塞 IO(BIO,Blocking IO)
最传统的方式 :发起 IO 请求后,线程一直等待,直到数据就绪或操作完成。
java
// BIO 读取文件
FileInputStream fis = new FileInputStream("file.txt");
byte[] buffer = new byte[1024];
int len = fis.read(buffer); // 阻塞!等待数据就绪
fis.close();
BIO 的问题:
- 线程在等待时什么都不干,浪费 CPU
- 每个连接一个线程,高并发下线程资源耗尽
text
BIO 模型:
客户端 1 → ─┐
客户端 2 → ─┼─→ 线程池 → 阻塞等待 → 读取数据
客户端 3 → ─┘
问题:线程数 = 连接数,连接多了线程不够用
2.2 非阻塞 IO(NIO,Non-blocking IO)
发起请求后立即返回,如果数据未就绪,返回「数据未就绪」状态,线程可以继续做其他事。
java
// NIO 读取文件(伪代码)
FileChannel channel = FileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer); // 非阻塞,立即返回
// 轮询检查数据是否就绪
while (buffer.hasRemaining()) {
// 处理数据
}
NIO 的优势:
- 少量线程可以处理大量连接
- 线程不会被阻塞,可以做其他事
text
NIO 模型:
客户端 1 → ─┐
客户端 2 → ─┼─→ Selector(单线程)→ 就绪事件通知
客户端 3 → ─┘
优势:一个线程管理多个连接
2.3 IO 多路复用(Multiplexing)
一个线程管理多个 IO 通道,通过 Selector 监听多个通道的就绪状态。
java
// 多路复用示例
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞,直到有事件就绪
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
}
}
}
多路复用的优势:
- 单线程管理大量连接
- Linux 下用 epoll,效率极高
2.4 异步 IO(AIO,Asynchronous IO)
完全异步:发起 IO 请求后,操作系统完成 IO 操作后通知应用程序。
java
// AIO 示例(Java 7+)
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
// IO 完成后自动调用
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
// 失败时调用
}
});
2.5 IO 模型对比
| 模型 | 阻塞 | 线程数 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| BIO | 是 | 1 连接 1 线程 | 简单 | 连接数少、低并发 |
| NIO | 否 | 1 线程 N 连接 | 中等 | 高并发连接 |
| 多路复用 | 否 | 1 线程 N 连接 | 中等 | 高并发连接(主流) |
| AIO | 否 | 事件驱动 | 复杂 | 极致性能 |
3. NIO 核心组件 🔧
3.1 Buffer(缓冲区)
NIO 的核心是 Buffer,数据从 Channel 读写到 Buffer。
java
// 创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024); // 堆内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 直接内存(零拷贝)
// 写入数据
buffer.put("hello".getBytes());
// 切换读模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 清空缓冲区
buffer.clear();
Buffer 的三个状态:
text
Buffer 状态:
│
├─ 写模式(flip 前):
│ position → 写入位置
│ limit → 容量
│ capacity → 容量
│
├─ 读模式(flip 后):
│ position → 读取位置
│ limit → 数据结束位置
│ capacity → 容量
│
└─ 清空(clear 后):
position → 0
limit → 容量
capacity → 容量
3.2 Channel(通道)
Channel 类似于 Stream,但可以非阻塞读写。
java
// 文件 Channel
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
// Socket Channel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// Server Socket Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
3.3 Selector(选择器)
Selector 监控多个 Channel 的状态,实现多路复用。
java
// 创建 Selector
Selector selector = Selector.open();
// 注册 Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 事件循环
while (true) {
selector.select(); // 阻塞,等待事件
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 新连接
} else if (key.isReadable()) {
// 可读
} else if (key.isWritable()) {
// 可写
}
}
}
3.4 直接内存 vs 堆内存
java
// 堆内存 Buffer(GC 管理)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 直接内存 Buffer(操作系统内存,零拷贝)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
直接内存的优势:
- 不经过 JVM 内存,直接和操作系统交互
- 减少一次内存拷贝(零拷贝)
- 适合大文件传输、网络通信
直接内存的劣势:
- 分配/释放成本高
- 不受 JVM 管理,需要手动释放
4. 常见 IO 场景实战 🎯
4.1 文件读取
java
// BIO 方式
try (BufferedReader reader = new BufferedReader(
new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// NIO 方式
try (FileChannel channel = FileChannel.open(
Paths.get("file.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) {
buffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
}
}
4.2 文件写入
java
// BIO 方式
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt"))) {
writer.write("Hello World");
}
// NIO 方式
try (FileChannel channel = FileChannel.open(
Paths.get("output.txt"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = StandardCharsets.UTF_8.encode("Hello World");
channel.write(buffer);
}
4.3 网络通信
java
// NIO Server
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
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);
client.read(buffer);
// 处理数据
}
}
}
4.4 大文件传输(零拷贝)
java
// 普通方式:需要两次拷贝
// 用户空间 → 内核缓冲区 → 磁盘
FileInputStream fis = new FileInputStream("file.txt");
OutputStream os = new FileOutputStream("output.txt");
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
// 零拷贝方式:一次拷贝
// 内核缓冲区 → 磁盘(直接 DMA)
FileChannel inChannel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("output.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE);
inChannel.transferTo(0, inChannel.size(), outChannel);
5. Spring Boot 中的 IO 操作 📦
5.1 文件上传
java
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
// 上传到本地
String path = "/uploads/" + file.getOriginalFilename();
file.transferTo(Paths.get(path));
// 或者用流
file.getInputStream(); // 获取输入流
return Result.success(path);
}
5.2 文件下载
java
@GetMapping("/download")
public ResponseEntity<Resource> download(String filename) throws IOException {
Path path = Paths.get("/files/" + filename);
Resource resource = new UrlResource(path.toUri());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
5.3 HTTP 请求(RestTemplate)
java
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 使用
String result = restTemplate.getForObject(
"http://localhost:8080/api/data",
String.class);
5.4 HTTP 请求(WebClient)
java
WebClient client = WebClient.create("http://localhost:8080");
Mono<String> result = client.get()
.uri("/api/data")
.retrieve()
.bodyToMono(String.class);
6. IO 性能优化建议 ⚡
6.1 减少 IO 次数
java
// ❌ 频繁 IO
for (String line : lines) {
writer.write(line); // 每次都 IO
}
// ✅ 批量 IO
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append("\n");
}
writer.write(sb.toString()); // 一次 IO
6.2 使用缓冲流
java
// ❌ 无缓冲
FileInputStream fis = new FileInputStream("file.txt");
// ✅ 有缓冲
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("file.txt"), 8192);
6.3 使用直接内存
java
// 大文件传输用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
6.4 异步 IO
java
// 同步阻塞 → 异步非阻塞
CompletableFuture.supplyAsync(() -> {
// 在线程池中执行 IO
return restTemplate.getForObject(url, String.class);
});
7. 常见面试题 📝
7.1 BIO 和 NIO 的区别?
- BIO:阻塞 IO,一个线程处理一个连接
- NIO:非阻塞 IO,一个线程处理多个连接(多路复用)
7.2 什么是零拷贝?
传统方式:用户空间 → 内核缓冲区 → 磁盘(两次拷贝)
零拷贝:内核缓冲区 → 磁盘(一次拷贝),通过 transferTo() 实现
7.3 Buffer 的 flip() 作用?
flip() 将 Buffer 从写模式切换到读模式,同时重置 position 到 0,limit 到数据结束位置。
7.4 Selector 的工作原理?
Selector 监听多个 Channel 的就绪状态(可读、可写、可连接),当有事件就绪时返回,应用程序可以处理对应的事件。
7.5 为什么 Netty 用 NIO?
- 高性能:一个线程管理大量连接
- 低资源:线程数不随连接数增长
- 零拷贝:支持直接内存
小结
- IO 是数据的输入输出,常见有磁盘 IO、网络 IO
- BIO 阻塞 IO,一个线程处理一个连接,高并发下性能差
- NIO 非阻塞 IO,通过 Buffer/Channel/Selector 实现多路复用
- 直接内存适合大文件传输和网络通信,减少内存拷贝
- 零拷贝 通过
transferTo()实现,减少一次内核拷贝 - Spring Boot 中文件上传用
MultipartFile,HTTP 请求用RestTemplate或WebClient
下一篇(020)预告 :Optional、Stream、Lambda:风格与性能注意点------函数式编程、Stream 惰性求值、性能陷阱。