Skip to content

NIO 简介

概述

NIO(New I/O) 自 JDK 1.4 引入,提供了一套面向缓冲区(Buffer)通道(Channel)的 IO 模型,与传统基于流(Stream)字节流字符流 形成互补。NIO 支持块(block)式读写、非阻塞 IO 以及多路复用(Selector),适合高并发、大文件或网络 IO 场景。本文介绍 NIO 的核心概念与基本用法,为后续学习网络编程或高性能 IO 打基础。

说明

「NIO」常指 java.nio 包下的 Buffer、Channel、Selector 等;NIO.2(Java 7)在 java.nio.file 中增加了 PathFilesFileChannel 的增强等,本文会简要提及。


NIO 与传统 IO 的对比

维度传统 IO(Stream)NIO
数据单位面向,逐字节或字节数组面向,以 Buffer 为单位读写
方向单向(InputStream/OutputStream 分开)Channel 可读可写(取决于实现)
阻塞默认阻塞:read/write 会阻塞线程支持非阻塞模式 + Selector 多路复用
典型用法顺序读写、简单文件/网络大文件、高并发、需要多路复用时的网络 IO

学习 NIO 时,重点理解 BufferChannel 的配合:数据从 Channel 读入 Buffer,或从 Buffer 写出到 Channel;Buffer 负责在内存中维护读写位置与界限。


核心概念:Buffer 与 Channel

Buffer(缓冲区)

Buffer 是一块连续内存,用于暂存要读入或写出的数据。最常用的是 ByteBuffer,另有 CharBufferIntBuffer 等。

Buffer 有三个关键属性(以写后读为例):

属性含义
capacity容量,创建时固定
position当前读写位置,随 get/put 移动
limit可读写的边界;写模式中 limit = capacity,读模式中 limit = 上次写到的 position

常用方法:

  • flip():写模式 → 读模式。将 limit = positionposition = 0,便于从刚写入的数据开头读。
  • clear():清空为写模式。position = 0limit = capacity(不擦除数据,只是重置可写区域)。
  • rewind()position = 0,不改 limit,用于重新读一遍。

提示

写数据后若要「从刚写的内容里读」,必须先 flip();读完若要再次写入同一 Buffer,可 clear()compact()(保留未读数据并转为写模式)。

Channel(通道)

Channel 表示与数据源/目标的连接,可从通道读入 Buffer 或把 Buffer 写出到通道。文件读写常用 FileChannel,通过 FileInputStream/FileOutputStream/RandomAccessFile.getChannel() 获得。Channel 通常需配合 Buffer 使用。


基本用法

创建 ByteBuffer

java
// 堆上分配,容量 1024 字节
ByteBuffer buf = ByteBuffer.allocate(1024);

// 直接内存(堆外),适合与 Channel 大量交换数据,减少拷贝
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);

从 FileChannel 读取到 Buffer

java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class NioReadExample {
    public static void main(String[] args) {
        Path path = Path.of("data/hello.txt");
        // try-with-resources 可同时管理 FileChannel(实现了 AutoCloseable)
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buf = ByteBuffer.allocate(256);
            int len;
            while ((len = channel.read(buf)) != -1) {
                buf.flip();           // 切换为读模式
                // 处理 buf 中 0..len 的数据,例如解码为字符串
                byte[] bytes = new byte[buf.remaining()];
                buf.get(bytes);
                System.out.print(new String(bytes));
                buf.clear();          // 清空以便下次读
            }
        } catch (IOException e) {
            System.err.println("读取失败: " + e.getMessage());
        }
    }
}

从 Buffer 写入 FileChannel

java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class NioWriteExample {
    public static void main(String[] args) {
        Path path = Path.of("data/out.txt");
        try (FileChannel channel = FileChannel.open(path,
                StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
            String text = "Hello, NIO!\n";
            ByteBuffer buf = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
            // 写出时 channel.write() 会从 buf 的 position 读到 limit
            while (buf.hasRemaining()) {
                channel.write(buf);
            }
        } catch (IOException e) {
            System.err.println("写入失败: " + e.getMessage());
        }
    }
}

ByteBuffer.wrap(byte[]) 会包装现有字节数组,position=0,limit=length,无需单独 allocate。


使用示例

示例 1:复制文件(Buffer + FileChannel)

java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class NioCopyFile {
    public static void copy(Path src, Path dest) throws IOException {
        try (FileChannel in = FileChannel.open(src, StandardOpenOption.READ);
             FileChannel out = FileChannel.open(dest,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
            ByteBuffer buf = ByteBuffer.allocate(8192);
            while (in.read(buf) != -1) {
                buf.flip();
                out.write(buf);
                buf.clear();
            }
        }
    }
}

示例 2:使用 NIO.2 的 Files 简化读写(Java 7+)

NIO.2 的 Files 类提供了便捷的读写方法,内部会使用 NIO,但 API 更简单:

java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

// 读所有字节
byte[] bytes = Files.readAllBytes(Path.of("data/hello.txt"));

// 按行读取(返回 List<String>)
Files.readAllLines(Path.of("data/hello.txt"), StandardCharsets.UTF_8);

// 写字节或字符串
Files.write(Path.of("data/out.txt"), "Hello".getBytes(StandardCharsets.UTF_8));
Files.writeString(Path.of("data/out.txt"), "Hello", StandardCharsets.UTF_8);  // JDK 11+

小文件或简单场景可直接用 Files;需要流式、大文件或精细控制时,用 Buffer + Channel


注意事项

注意

使用 Buffer 时务必注意 flip/clear/rewind 的时机:写完后要读必须先 flip();读完后要再写需 clear()compact(),否则 position/limit 错乱会导致读不到数据或覆盖错误。

注意

FileChannel 通过 FileChannel.open() 或从流/RAF 的 getChannel() 获得,使用完毕后应关闭。用 try-with-resources 可自动关闭,避免资源泄漏。详见 异常处理 - try-with-resources

提示

  • allocateDirect() 分配的堆外内存适合与 Channel 大量交换数据,但创建和销毁成本较高,一般用于长期存在、大块读写的 Buffer。
  • 网络 IO 中的 SocketChannelServerSocketChannelSelector 结合可实现非阻塞多路复用,属于进阶内容,可在掌握 FileChannel 与 Buffer 后再学。

相关链接

基于 VitePress 构建