欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 【Java NIO】

【Java NIO】

2025/4/11 15:51:36 来源:https://blog.csdn.net/tnt87/article/details/146939666  浏览:    关键词:【Java NIO】

饮酒

  • 概要
  • 为什么需要 NIO?传统 I/O 的局限性
    • 阻塞式 I/O(BIO)的问题
    • 内存映射与零拷贝的缺失
      • 传统IO的的完整步骤
        • 传统IO数据流向
        • 为什么需要两次拷贝?(内核缓冲区→用户缓冲区)
        • 性能瓶颈分析
      • 内存映射
      • 零拷贝
    • 非阻塞与事件驱动需求

东晋 · 陶渊明

​结 庐 在 人 境,而 无 车 马 喧。
问 君 何 能 尔?心 远 地 自 偏。
采 菊 东 篱 下,悠 然 见 南 山。
山 气 日 夕 佳,飞 鸟 相 与 还。
此 中 有 真 意,欲 辨 已 忘 言。

概要

Java I/O support is included in the java.io and java.nio packages. Together these packages include the following features:

  • Input and output through data streams, serialization and the file system.
  • Charsets, decoders, and encoders, for translating between bytes and Unicode characters.
  • Access to file, file attributes and file systems.
  • APIs for building scalable servers using asynchronous or multiplexed, non-blocking I/O.

为什么需要 NIO?传统 I/O 的局限性

阻塞式 I/O(BIO)的问题

  • 传统 InputStream/OutputStream 是同步阻塞模型:线程在读写数据时必须等待操作完成,虽然该线程并不会占用CPU时间片,但是该线程就一直不能释放,无法处理其他任务。
import java.io.FileInputStream;
import java.io.InputStream;public class BlockingExample {public static void main(String[] args) {try (InputStream is = new FileInputStream("large_file.txt")) {byte[] buffer = new byte[1024];int bytesRead;// 同步阻塞点:read() 会阻塞直到数据读取完成while ((bytesRead = is.read(buffer)) != -1) { System.out.println("读取到 " + bytesRead + " 字节数据");// 模拟处理数据(假设处理耗时)Thread.sleep(1000); }} catch (Exception e) {e.printStackTrace();}}
}
  • 高并发场景下(如服务器处理大量连接),需为每个连接分配独立线程,导致线程资源耗尽和上下文切换开销。
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;public class BlockingSocketServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8080)) {while (true) {// 同步阻塞点 1:accept() 阻塞,直到有客户端连接Socket clientSocket = serverSocket.accept(); System.out.println("客户端连接:" + clientSocket.getRemoteSocketAddress());// 为每个客户端连接分配一个线程处理(传统 BIO 的典型做法)new Thread(() -> {try (InputStream is = clientSocket.getInputStream()) {byte[] buffer = new byte[1024];int bytesRead;// 同步阻塞点 2:read() 阻塞,直到客户端发送数据while ((bytesRead = is.read(buffer)) != -1) { String data = new String(buffer, 0, bytesRead);System.out.println("收到数据:" + data);}} catch (Exception e) {e.printStackTrace();}}).start();}} catch (Exception e) {e.printStackTrace();}}
}

内存映射与零拷贝的缺失

传统 I/O 需要多次数据拷贝(用户态 ↔ 内核态),而 NIO 的 FileChannel 支持内存映射文件(MappedByteBuffer),减少拷贝次数,提升性能。

传统IO的的完整步骤

假设程序调用FileInputStream.read()读取一个文件,以下是底层发生的详细过程:

  1. 用户态发起读取请求
try (FileInputStream fis = new FileInputStream("data.bin")) {byte[] buffer = new byte[1024]; // 用户缓冲区fis.read(buffer); // 触发系统调用
}
  1. 进入内核态(系统调用)
  • 程序执行read()时,会触发系统调用(如Linux的read()),从用户态切换到内核态。

  • 切换代价:CPU需要保存用户态寄存器状态、权限提升、内核栈切换等。

  1. 内核从磁盘读取数据

  内核检查文件缓存(Page Cache):

  • 如果文件数据已缓存在内核的Page Cache中,直接跳到步骤4。

  • 如果未缓存,发起磁盘I/O请求。

  磁盘DMA操作:

  • 内核通过**DMA(Direct Memory Access)**控制器,将数据从磁盘直接拷贝到内核缓冲区(Page Cache),无需CPU参与。

  • 磁盘 → 内核缓冲区(Page Cache)的传输由DMA完成。

  1. 数据从内核缓冲区拷贝到用户缓冲区
  • CPU将数据从内核的Page Cache拷贝到用户态提供的缓冲区(即Java中的byte[] buffer)。

  • 这是传统I/O的关键性能瓶颈:一次冗余的CPU拷贝。

  1. 返回用户态
  • 系统调用结束,内核态切换回用户态。

  • 程序继续执行,用户缓冲区中包含文件数据

数据流向示意图

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class MemoryMapExample {public static void main(String[] args) throws Exception {try (RandomAccessFile file = new RandomAccessFile("large_file.bin", "rw");FileChannel channel = file.getChannel()) {// 将文件映射到内存(模式:读写,映射范围:0~文件末尾)MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());// 直接操作内存(无需显式read/write)buffer.putInt(0, 123);  // 修改文件开头的数据int value = buffer.getInt(0); // 读取数据System.out.println("Read value: " + value);}}
}
传统IO数据流向

磁盘文件 → DMA拷贝 → 内核缓冲区(Page Cache) → CPU拷贝 → 用户缓冲区(byte[]) → 程序使用

为什么需要两次拷贝?(内核缓冲区→用户缓冲区)
  • 安全隔离:用户态程序不能直接访问内核内存(防止恶意程序破坏系统稳定性)。

  • 兼容性:内核需要统一管理所有进程的I/O,缓存数据可能被多个进程共享。

性能瓶颈分析
操作代价
用户态/内核态切换每次read()/write()均需切换
数据拷贝(CPU)内核缓冲区 ↔ 用户缓冲区的冗余拷贝
小文件高频访问切换和拷贝开销占比极高

内存映射

原理:

  • 将文件的一部分或全部 直接映射到 进程的虚拟内存空间,使得文件可以像访问内存一样读写。

  • 底层机制:操作系统通过 页缓存(Page Cache) 实现,文件数据不会直接加载到 JVM 堆内存,而是由 OS 管理。

  • 优势:避免 read()/write() 的系统调用开销;适合 大文件随机访问(如数据库、日志文件)

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;public class MemoryMappedIO {public static void main(String[] args) throws IOException {try (FileChannel channel = FileChannel.open(Paths.get("large_file.bin"), StandardOpenOption.READ)) {// 将文件映射到内存(READ_ONLY模式)MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());// 直接操作内存(无需显式read()调用)while (buffer.hasRemaining()) {byte b = buffer.get(); // 触发缺页中断,由内核自动加载数据processByte(b);}}}private static void processByte(byte b) {// 模拟数据处理}
}

执行流程:

  • 映射建立:channel.map()仅在虚拟内存中建立文件映射,不立即加载数据。

  • 按需加载:访问buffer.get()时触发缺页中断,由内核将文件数据加载到物理内存。

  • 零拷贝:数据直接从磁盘→物理内存,无需经过用户缓冲区。

对比总结

特性传统I/O内存映射
数据拷贝次数2次(内核→用户)1次(磁盘→物理内存)
系统调用/切换每次read()均需切换仅缺页中断时由内核处理
内存占用用户缓冲区需预分配由操作系统按需分页
适用场景小文件、顺序读写大文件、随机访问

零拷贝

零拷贝:通过操作系统特性(如 sendfile、mmap)避免数据在 用户态和内核态之间的拷贝,直接在内核完成传输。

方法1:FileChannel.transferTo()(推荐)

import java.io.*;
import java.nio.channels.*;public class ZeroCopyExample {public static void main(String[] args) throws IOException {try (FileChannel fileChannel = new FileInputStream("large_file.bin").getChannel();SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {// 零拷贝:直接从文件通道传输到Socket通道fileChannel.transferTo(0, fileChannel.size(), socketChannel);}}
}

数据流向

磁盘 → DMA拷贝 → 内核缓冲区(Page Cache) → DMA拷贝 → 网卡

优化点:

仅2次DMA拷贝(无CPU拷贝)。

1次系统调用(transferTo),无用户态数据参与。

方法2:MappedByteBuffer(内存映射 + Socket发送)

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.net.*;public class MappedZeroCopyExample {public static void main(String[] args) throws IOException {try (FileChannel fileChannel = FileChannel.open(Paths.get("large_file.bin"), StandardOpenOption.READ);SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {// 内存映射文件MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());// 直接从内存缓冲区写入SocketsocketChannel.write(buffer);}}
}

数据流向
磁盘 → DMA拷贝 → 物理内存(Mapped Buffer) → DMA拷贝 → 网卡
优化点:

避免用户缓冲区拷贝,但仍需数据从内存→Socket的传输。
实际应用:

Kafka 使用 零拷贝 加速日志文件传输。

Elasticsearch 使用 内存映射 快速访问索引文件。

非阻塞与事件驱动需求

  传统 I/O 无法实现非阻塞操作,难以应对高并发、实时性要求高的场景(如聊天服务器、实时交易系统)。

三、NIO 的典型应用场景
高性能网络服务器

如 Netty、Tomcat NIO Connector,支持高并发连接。

实时数据处理

金融交易系统、实时日志分析。

大文件高效读写

内存映射文件加速大文件访问。

四、学习路径与知识点

  1. 基础篇
    Buffer 的分配与操作

allocate() vs allocateDirect()(堆内存 vs 直接内存)

flip(), clear(), rewind() 方法的作用。

Channel 的类型与使用

FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。

Selector 的事件监听

OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE。

  1. 进阶篇
    NIO 网络编程模型

Reactor 模式(单线程、多线程、主从多线程)。

粘包/拆包问题

自定义协议解决 TCP 数据流边界问题。

NIO 与 AIO(异步 I/O)对比

AIO 的 AsynchronousChannel 和 CompletionHandler。

  1. 实战篇
    实现一个简易 NIO 服务器
public class NioServer {public static void main(String[] args) throws IOException {ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.bind(new InetSocketAddress(8080));serverChannel.configureBlocking(false); // 非阻塞模式Selector selector = Selector.open();serverChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select(); // 阻塞直到有事件Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> iter = keys.iterator();while (iter.hasNext()) {SelectionKey key = iter.next();if (key.isAcceptable()) {// 处理新连接SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {// 处理读事件SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);clientChannel.read(buffer);buffer.flip();process(buffer); // 处理数据}iter.remove();}}}
}

性能调优

调整 Buffer 大小、使用直接内存、优化 Selector 轮询策略。
五、学习资源推荐

  1. 官方文档
    Java NIO 官方教程

java.nio 包 Javadoc:重点阅读 Buffer、Channel、Selector 类。

  1. 书籍
    《Java NIO》(Ron Hitchens):详解 NIO 核心概念与实战。

《Netty 权威指南》(李林锋):结合 Netty 框架学习 NIO 应用。

  1. 在线教程
    Java NIO Tutorial - Jenkov.com

NIO 入门 - IBM Developer

  1. 视频课程
    Java NIO 编程入门 - B站/慕课网(搜索关键词)

六、NIO 的局限性
编码复杂度高

需手动管理 Buffer、处理事件循环,易出错。

调试困难

非阻塞逻辑和异步回调增加调试难度。

平台兼容性

某些操作系统对 Selector 的实现存在差异(如 Linux epoll vs Windows IOCP)。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词