前置知识: Java

JavaIO与NIO

00:00
3 min Intermediate 2026/6/14

BIO、NIO与AIO

概述

Java 的 I/O 体系经历了从传统 BIO 到 NIO 再到 AIO 的演进。BIO 是最早的阻塞式模型,每个连接占用一个线程;NIO 引入了多路复用机制,一个线程可管理多个连接;AIO 则进一步实现了真正的异步非阻塞操作。理解三者的差异与适用场景,是编写高性能网络与文件程序的基础。

基础概念

BIO vs NIO vs AIO

模型说明适用场景
BIO同步阻塞,一个连接一个线程连接数少且固定的场景
NIO同步非阻塞,Selector 多路复用并发短连接
AIO异步非阻塞,回调通知并发长连接

核心术语

  • Buffer(缓冲区NIO 中数据的容器,所有读写操作通过缓冲区完成
  • Channel通道:双向的数据通道,可同时进读写
  • Selector选择器多路复用器,用于监控多个 Channel事件
  • SelectionKey表示 ChannelSelector 之间的注册关系

快速上手

BIO 文件读取

// 传统 BIO 方式读取文件
try (FileInputStream fis = new FileInputStream("data.txt")) {
    byte[] buffer = new byte[1024];
    int bytesRead = fis.read(buffer);
    while (bytesRead != -1) {
        // 处理读取到的数据
        System.out.println(new String(buffer, 0, bytesRead));
        bytesRead = fis.read(buffer);
    }
} catch (IOException e) {
    e.printStackTrace();
}

NIO 文件读取

// NIO 方式读取文件
try (FileChannel channel = FileChannel.open(
        Path.of("data.txt"), StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocate(1024);
    while (channel.read(buf) != -1) {
        buf.flip(); // 切换为读模式
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        buf.clear(); // 清空缓冲区,准备下次写入
    }
} catch (IOException e) {
    e.printStackTrace();
}

详细用法

Buffer 的核心操作

// 创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);  // 堆内缓冲区
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024); // 直接缓冲区(堆外内存)

// 写入数据
buf.put((byte) 65);
buf.put(new byte[]{66, 67, 68});

// 切换为读模式
buf.flip();

// 读取数据
byte b = buf.get();       // 读取单个字节
byte[] arr = new byte[3];
buf.get(arr);             // 读取到数组

// 常用方法
buf.rewind();    // 重读:position 归零,limit 不变
buf.mark();      // 标记当前位置
buf.reset();     // 回到标记位置
buf.compact();   // 将未读数据移到缓冲区头部

Channel 类型

// 文件通道
FileChannel fileChannel = FileChannel.open(
    Path.of("data.txt"),
    StandardOpenOption.READ,
    StandardOpenOption.WRITE
);

// 文件通道传输数据
try (FileChannel src = FileChannel.open(Path.of("source.txt"), StandardOpenOption.READ);
     FileChannel dest = FileChannel.open(Path.of("dest.txt"),
         StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    // 零拷贝传输,效率高
    dest.transferFrom(src, 0, src.size());
}

// Socket 通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 设置为非阻塞模式

Selector 多路复用

// 创建 Selector 并注册通道
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
// 注册接受连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 事件循环
while (true) {
    int readyCount = selector.select(); // 阻塞直到有事件就绪
    if (readyCount == 0) continue;

    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> iter = selectedKeys.iterator();
    while (iter.hasNext()) {
        SelectionKey key = iter.next();
        if (key.isAcceptable()) {
            // 处理新连接
            SocketChannel client = serverChannel.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            // 处理读事件
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(256);
            int bytesRead = client.read(buf);
            if (bytesRead == -1) {
                client.close(); // 连接关闭
            }
        }
        iter.remove(); // 必须手动移除已处理的键
    }
}

常见场景

文件复制

// 使用 NIO 进行文件复制(推荐方式)
public static void copyFile(Path source, Path target) throws IOException {
    try (FileChannel srcChannel = FileChannel.open(source, StandardOpenOption.READ);
         FileChannel destChannel = FileChannel.open(target,
             StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
        long size = srcChannel.size();
        long transferred = 0;
        while (transferred < size) {
            // 使用 transferTo 实现零拷贝
            transferred += srcChannel.transferTo(transferred, size - transferred, destChannel);
        }
    }
}

内存映射文件

// 大文件处理:使用内存映射提升性能
try (FileChannel channel = FileChannel.open(
        Path.of("large.dat"),
        StandardOpenOption.READ,
        StandardOpenOption.WRITE)) {
    // 将文件映射到内存
    MappedByteBuffer mappedBuf = channel.map(
        FileChannel.MapMode.READ_WRITE, 0, channel.size());
    // 像操作内存一样操作文件
    mappedBuf.putInt(0, 42);
    mappedBuf.force(); // 强制将修改写入磁盘
}

注意事项

  • Buffer 的 flip() 方法容易遗漏,忘记调用会导致读取到空数据或错误数据
  • Selector 事件循环中必须手动移除已处理的 SelectionKey,否则会重复处理
  • 直接缓冲区(allocateDirect)分配销毁开销大,适合长期使用的大缓冲区
  • 内存映射文件修改后需要调用 force() 确保数据落盘
  • NIO 的非阻塞模式等于异步,AIO 才是真正的异步操作
  • 在 Windows 上 AIO 的实现效果不如 Linux,选择模型时需考虑部署平台

进阶用法

AIO 异步文件操作

// 使用 AsynchronousFileChannel 进行异步读写
try (AsynchronousFileChannel asyncChannel = AsynchronousFileChannel.open(
        Path.of("data.txt"), StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocate(1024);
    // 异步读取,传入回调
    asyncChannel.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() {
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            attachment.flip();
            System.out.println("读取完成,字节数: " + result);
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            System.err.println("读取失败: " + exc.getMessage());
        }
    });
}

NIO 与线程池配合

// 将 I/O 操作与业务处理分离
ExecutorService workerPool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors());

// 在 Selector 事件循环中,将读到的数据交给线程池处理
while (true) {
    selector.select();
    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(4096);
            int bytesRead = client.read(buf);
            if (bytesRead > 0) {
                buf.flip();
                byte[] data = new byte[buf.remaining()];
                buf.get(data);
                // 将业务处理提交到线程池,避免阻塞 I/O 线程
                workerPool.submit(() -> process(data));
            }
        }
    }
    selector.selectedKeys().clear();
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式