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


原始标题:Java IO vs NIO