适用读者:写文件读写、或要理解 Netty/Reactor 的人。
前置:S04-B07、S02-B04(TCP 可靠传输)。
覆盖旧文:A-1687(198) A-1688(108) A-1684(128) A-1680(148) A-1682(145) A-1706(177) A-1709(89) A-1707(108) A-1708(188) A-1725(99) A-1723(159) A-2332(227) = 1,876 点;A-632(OkHttp)→ S06duplicate_ref。
覆盖扩展:E-S04-018(四种 IO 模型)。
上一篇:S04-J05 · SPI 与运行时可扩展系统设计
下一篇:S04-A01 · Java 版本演进与团队基线。
1. 一句话结论
IO 的本质是"数据在用户态缓冲区和内核缓冲区之间搬运"。四种模型的差别,只在"谁等、等多久、一次等几个"。
| 模型 | 谁在等 | 一次等几个连接 |
|---|---|---|
| 阻塞 IO(BIO) | 线程 | 1 |
| 非阻塞 IO(NIO) | 线程轮询 | 1(但可循环) |
| IO 多路复用(epoll/select) | 内核帮忙等,线程休眠 | N 个 |
| 异步 IO(AIO) | 内核完成后再通知 | N 个 |
"Java 的 NIO"其实是"IO 多路复用 + Buffer/Channel 抽象",不是"异步 IO"。这个命名容易误导。
2. 传统 IO:流(Stream)与装饰器
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
String line;
while ((line = r.readLine()) != null) { /* ... */ }
}
| 类族 | 单位 | 典型 |
|---|---|---|
| 字节流 | byte |
InputStream/OutputStream、FileInputStream |
| 字符流 | char |
Reader/Writer、FileReader |
| 缓冲流 | 装饰器 | BufferedInputStream(减少系统调用) |
装饰器模式的价值:new BufferedReader(new InputStreamReader(in, UTF_8)) —— 缓冲、编码、字节流三者自由组合。"缓冲"是最值得加的一层(一次读 8KB vs 每次读 1 字节)。
3. NIO 三件套:Channel / Buffer / Selector
| 组件 | 作用 | 与传统 IO 的差别 |
|---|---|---|
| Channel | 双向通道 | 流是单向的 |
| Buffer | 数据容器(position/limit/capacity) |
数据先读到 Buffer 再处理 |
| Selector | 多路复用器 | 一个线程管多个 Channel |
Buffer 的三个指针(最容易混):
flip() 写模式 → 读模式(limit = position, position = 0)
clear() 读模式 → 写模式(position = 0, limit = capacity)
compact() 保留未读数据,准备继续写
判据:"忘了 flip()"是 NIO 的第一大 bug——写完直接读,读到的是 0 字节。
4. 四种 IO 模型与 Java 的对应(E-S04-018)
| 模型 | Linux 机制 | Java 对应 |
|---|---|---|
| 阻塞 | read 阻塞 |
InputStream.read() |
| 非阻塞 | O_NONBLOCK |
channel.configureBlocking(false) |
| 多路复用 | select/poll/epoll |
Selector(底层 epoll) |
| 异步 | io_uring/AIO |
AsynchronousFileChannel(平台支持有限) |
为什么 NIO 能撑高并发:一个线程 + 一个 Selector 可以管理上万个连接,因为"等待"由内核完成,线程只在"有事件"时被唤醒(避免"每连接一线程"的栈内存与切换成本,S02-B02)。
5. 网络编程:从 Socket 到 HTTP 服务器(A-1725/A-1723)
| 层次 | 用到的类 |
|---|---|
| Socket(阻塞) | ServerSocket + Socket |
| NIO | ServerSocketChannel + Selector |
| HTTP 解析 | 手写解析请求行/头/体(教学用) |
实测级判据:"手写 HTTP 服务器"是理解协议最快的方式——你会立刻明白 B05 讲的"消息边界"为什么必须由协议定义(Content-Length/chunked)。
6. 实践建议
| 场景 | 选择 |
|---|---|
| 普通文件读写 | 传统 IO + Buffered*(简单够用) |
| 大文件拷贝 | FileChannel.transferTo(零拷贝,不走用户态) |
| 高并发网络 | NIO + Selector(或直接用 Netty) |
| 业务代码 | 用成熟框架(Netty/Spring WebFlux),别自己写 Selector 循环 |
7. 五个误区
| 误区 | 真相 |
|---|---|
| "NIO 就是异步 IO" | 是"New IO"(多路复用 + Buffer) |
| "NIO 一定更快" | 小数据量下传统 IO 更快(Buffer 分配成本) |
| "Buffer 写完就能读" | 必须 flip() |
| "加缓冲没意义" | 显著减少系统调用次数 |
| "自己写 Selector 循环更可控" | 边界情况极多,优先用 Netty |
8. 自检
- 四种 IO 模型的"谁在等、等几个"分别是什么?
- 为什么说 Java NIO 不是异步 IO?
- Buffer 的
flip()做了什么?不加会怎样? - 为什么"每连接一个线程"撑不了高并发?
- 大文件拷贝为什么用
transferTo?
9. 下一篇
S04-A01 · Java 版本演进与团队基线
进阶层至此结束。下一篇进入架构师层:8→11→17→21 升不升、升了得到什么、风险在哪。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-1687/1688/1684/1680/1682/1706/1709/1707/1708/1725/1723/2332(A-632 → S06);扩展点 E-S04-018。