1. 概述
IO(输入/输出)是 Java 开发中最常见的任务之一。本文将深入对比 Java 中两个核心的 IO 体系:经典的 **java.io
**(即传统 IO)与后来引入的 **java.nio
**(NIO),重点分析它们在网络通信中的差异与适用场景。
对于有经验的开发者来说,理解这两者的底层机制,能帮你避免踩坑,尤其在高并发、高性能场景下做出更合理的技术选型。
2. 核心特性对比
我们先从两个包的核心能力入手,搞清楚它们“能做什么”。
2.1. 传统 IO – java.io
✅ 引入时间早:java.io
包从 Java 1.0 就已存在,Reader
/Writer
在 1.1 加入。
✅ 面向流(Stream)设计:以字节流或字符流的方式逐字节处理数据。
✅ 阻塞式(Blocking):每次读写操作都会阻塞线程,直到数据完整到达或写完。
主要组件包括:
InputStream
/OutputStream
:以字节为单位读写数据Reader
/Writer
:字符流封装,便于处理文本- 所有操作默认是 阻塞的,适合简单场景,但并发性能差
2.2. NIO – java.nio
✅ 引入于 Java 1.4,并在 Java 7(NIO.2)大幅增强,支持异步 IO 和更强大的文件操作。
✅ 面向缓冲区与通道(Buffer & Channel):不再以流为中心,而是通过 Buffer 批量读写,Channel 作为数据通道。
✅ 支持非阻塞模式(Non-blocking):可以轮询多个连接,实现单线程处理多连接(即 I/O 多路复用)。
✅ 支持内存映射、直接内存访问:性能更高,适合大文件或高吞吐场景。
核心组件包括:
Buffer
:数据容器,支持批量读写Channel
:双向数据通道(如SocketChannel
)Selector
:实现 I/O 多路复用,监听多个 Channel 的就绪状态CharsetDecoder
:字节与字符的编码转换AsynchronousSocketChannel
(NIO.2):真正的异步非阻塞通信
⚠️ 简单粗暴总结:
java.io
是“来一个请求,开一个线程”;java.nio
是“一个线程管一堆连接”,更适合高并发。
3. 测试环境搭建
为了独立测试,我们使用 WireMock 模拟一个 HTTP 服务,避免依赖真实服务器。
3.1. 添加依赖
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.26.3</version>
<scope>test</scope>
</dependency>
3.2. 配置 Mock Server
使用 JUnit 配置一个动态端口的 WireMock 服务,并预设一个 JSON 响应:
@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
private String REQUESTED_RESOURCE = "/test.json";
@Before
public void setup() {
stubFor(get(urlEqualTo(REQUESTED_RESOURCE))
.willReturn(aResponse()
.withStatus(200)
.withBody("{ \"response\" : \"It worked!\" }")));
}
这样,任何对 /test.json
的 GET 请求都会收到预设的 JSON 响应。
4. 阻塞 IO 实践 – java.io
我们通过 Socket
发起请求,演示传统 IO 的阻塞读写流程。
4.1. 发送请求
使用 Socket
连接服务端,通过 OutputStream
发送 HTTP 请求:
Socket socket = new Socket("localhost", wireMockRule.port());
OutputStream clientOutput = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));
writer.print("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n");
writer.flush();
🔍 注意:HTTP/1.0 需要手动加
\r\n\r\n
表示请求头结束。
4.2. 读取响应(阻塞等待)
使用 InputStream
+ BufferedReader
逐行读取响应,直到流结束:
InputStream serverInput = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));
StringBuilder ourStore = new StringBuilder();
for (String line; (line = reader.readLine()) != null;) {
ourStore.append(line);
ourStore.append(System.lineSeparator());
}
✅ 优点:代码简单,易于理解。
❌ 缺点:readLine()
会一直阻塞,直到收到完整一行或连接关闭。如果数据没来,线程就卡住,无法处理其他任务。
5. 非阻塞 IO 实践 – java.nio
现在我们用 NIO 实现同样的功能,但采用非阻塞方式。
5.1. 发送请求
使用 SocketChannel
替代 Socket
,并直接写入 ByteBuffer:
InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());
SocketChannel socketChannel = SocketChannel.open(address);
Charset charset = StandardCharsets.UTF_8;
socketChannel.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));
🔍
SocketChannel
默认是阻塞模式,可通过configureBlocking(false)
切换为非阻塞。
5.2. 读取响应(非阻塞循环)
使用 ByteBuffer
接收数据,通过 Selector
或直接轮询读取:
ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(8192);
StringBuilder ourStore = new StringBuilder();
while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
byteBuffer.flip(); // 切换为读模式:position=0, limit=写入位置
storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore);
byteBuffer.compact(); // 压缩缓冲区,未处理数据前移,position设为有效数据末尾
}
⚠️ 关键点:
flip()
:将 Buffer 从写模式切换为读模式compact()
:保留未处理的数据,准备下一次读取read()
返回-1
表示连接关闭,0
表示无数据可读(非阻塞时常见)
最后别忘了关闭 Channel:
socketChannel.close();
5.3. 缓冲区数据处理
将字节缓冲区解码为字符并追加到结果中:
void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer,
CharsetDecoder charsetDecoder, StringBuilder ourStore) {
charsetDecoder.decode(byteBuffer, charBuffer, true);
charBuffer.flip();
ourStore.append(charBuffer);
charBuffer.clear(); // 清空,准备下次写入
}
✅
decode()
将字节转为字符,true
表示流结束(flush) ✅clear()
重置 CharBuffer 的 position 和 limit
6. 总结
特性 | java.io |
java.nio |
---|---|---|
数据模型 | Stream(流) | Buffer + Channel(缓冲区 + 通道) |
IO 模式 | 阻塞 | 支持非阻塞、异步 |
并发能力 | 差(每连接一线程) | 强(I/O 多路复用) |
性能 | 一般 | 更高(尤其大文件、高并发) |
编程复杂度 | 低 | 高(需手动管理 Buffer) |
📌 选型建议:
- ✅ 普通应用、小并发:用
java.io
,简单直接 - ✅ 高并发、高性能服务(如网关、RPC):必须用
java.nio
或基于它的框架(Netty) - ✅ 文件大、吞吐高:优先考虑
MappedByteBuffer
或异步 IO
📌 最后提醒:NIO 虽强,但不要为了“技术先进”而滥用。理解本质,按需使用,才是高级开发的标配。
完整示例代码已上传至 GitHub:https://github.com/yourname/java-io-nio-demo