IO 与 NIO:从阻塞流到多路复用
更新时间:2026-09-20。Java 处理数据进出有两代 API:传统
java.io(BIO,面向流、阻塞)和java.nio(面向缓冲区、可非阻塞、支持多路复用)。高并发网络编程的基石是后者。
一、BIO:面向流、阻塞
bash
字节流(8 位):InputStream / OutputStream —— 一切的基础
字符流(16 位 char):Reader / Writer —— 处理文本,带编码
桥接:InputStreamReader 把字节流按字符集解码成字符流java
// try-with-resources 自动关闭;字节流读文件
try (InputStream in = new FileInputStream("a.bin")) {
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) { /* 处理 buf[0..n) */ }
}
// 带缓冲的字符流读文本,指定 UTF-8 避免平台默认编码坑
try (BufferedReader br = Files.newBufferedReader(Path.of("a.txt"), StandardCharsets.UTF_8)) {
br.lines().forEach(System.out::println);
}特点:read() 在数据没到时阻塞线程;一连接一线程,连接上万时线程数爆炸、内存与上下文切换成本高。网络层面的连接与超时问题见 strace 网络超时。
二、NIO:Channel + Buffer + Selector
| 概念 | 作用 |
|---|---|
| Channel | 双向读写通道(FileChannel/SocketChannel/ServerSocketChannel) |
| Buffer | 一块内存(ByteBuffer),读写都经过它,有 position/limit/flip |
| Selector | 一个线程轮询多个 Channel 的就绪事件(多路复用) |
| SelectionKey | Channel 与 Selector 的绑定,标记 OP_READ/WRITE/ACCEPT/CONNECT |
java
Selector selector = Selector.open();
ServerSocketChannel srv = ServerSocketChannel.open();
srv.configureBlocking(false); // 切到非阻塞
srv.bind(new InetSocketAddress(8080));
srv.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞到至少一个通道就绪
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey k = it.next();
it.remove();
if (k.isAcceptable()) { /* accept 并注册 OP_READ */ }
else if (k.isReadable()) { /* 读到的都是已就绪的,不阻塞 */ }
}
}
核心收益:用极少量线程管理海量连接,只有真正就绪的通道才被处理,避免空等。Buffer 操作要记 flip()(写模式切读模式:limit=position, position=0)和 clear()/compact()。
三、零拷贝 transferTo
FileChannel.transferTo() 在底层走 sendfile,数据直接从页缓存到 socket,绕开用户态拷贝和多次上下文切换——这是 Kafka/Nginx 高吞吐的关键,原理见零拷贝 sendfile。
四、选型
- 普通文件读写、小服务:用
java.nio.file.Files+ BIO 缓冲流即可,简单不易错。 - 海量长连接网关/IM/推送:NIO 多路复用;实际项目几乎不裸写 Selector,直接用 Netty(在 NIO 之上处理拆包、epoll 空轮询 bug、字节缓冲池)。
- 高吞吐文件传输:transferTo/mmap 零拷贝。
坑
- 忘记 flip():Buffer 写完直接读,position 还在末尾,读不到数据或读到 0 字节。
- 字符流不传字符集:默认用平台编码,Windows/Linux 行为不一致导致乱码,显式 StandardCharsets.UTF_8。
- 把 NIO 当万能:计算密集或连接很少时,多路复用的复杂度不划算,BIO+线程池更直白。
- selectedKeys 不 remove:同一个 key 被重复处理,必须手动从 selectedKeys 移除。
- 在通道上做阻塞操作:configureBlocking(false) 后仍调用会阻塞的方法,破坏反应器模型。
总结
java.io 是面向流的阻塞模型,字节流处理二进制、字符流配合字符集处理文本,一连接一线程撑不住高并发;java.nio 用双向 Channel、带 position/limit 的 Buffer 和单线程轮询多连接的 Selector 实现 IO 多路复用,让少量线程管理海量就绪连接,并以 transferTo 提供 sendfile 零拷贝;日常文件用 Files+缓冲流,超高并发网络直接上 Netty,同时注意 flip、显式 UTF-8 与及时移除 selectedKeys。