2、Java NIO
1、Java NIO 简介
Buffer 影响编码(byte、long、直接缓冲区),Channel 影响来源(阻塞、非阻塞、异步)
1.1、简介
Java NIO(New IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API
NIO 与原来的 IO 有同样的作用和目的,但是使用方式完全不同,NIO 支持面向缓冲区的、基于通道的 IO 操作
NIO 将以更加高效的方式进行文件的读写操作
1.2、NIO 与 IO 的区别
IO:面向流(Stream Oriented)、阻塞 IO(Blocking IO)
NIO:面向缓冲区(Buffer Oriented)、非阻塞 IO(Non Blocking IO)、选择器(Selectors)
2、NIO 重要知识点详细介绍
2.1、通道和缓冲区
Java NIO 系统的核心在于:通道(Channel)和缓冲区(Buffer)
- 通道表示打开到 IO 设备(例如:文件、套接字)的连接
- 若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区
- 然后操作缓冲区,对数据进行处理
- 简而言之:Channel 负责传输、Buffer 负责存储
2.2、面向流和面向缓冲区解释
传统 IO 流:面向流
- 我们需要把磁盘文件或者网络文件中的数据读取到程序中来
要建立一个用于传输数据的管道,我们弄了一个 byte 数组,来回进行数据传递
所以说原来的 IO 面对的就是管道里面的一个数据流,所以我们说原来的 IO 是面向流的 - 我们说传统的 IO 还有一个特点就是,它是单向的
如果说我们想把目标地点的数据读取到程序中来:我们需要建立一个管道,这个管道我们称为输入流
如果如果我们程序中有数据想要写到目标地点去:我们得再建立一个管道,这个管道我们称为输出流
所以我们说传统的 IO 流是单向的
NIO:面向缓冲区
- 只要是 IO,那么就是为了完成数据传输的,即便用 NIO,它也是为了数据传输,所以你要想完成数据传输,你也得建立一个用于传输数据的通道
这个通道你不能把它理解为之前的水流了,但是你可以把它理解为铁路
铁路本身是不能完成运输的,铁路要想完成运输必须依赖火车,说白了这个通道就是为了连接源地点和目标地点
注意:通道本身不能传输数据,要想传输数据必须要有缓冲区,这个缓冲区你就可以完全把它理解为火车 - 你现在想把程序中的数据写到文件中:把数据都写到缓冲区,然后缓冲区通过通道进行传输,最后再把数据从缓冲区拿出来写到文件中
- 你想把文件中的数据传数到程序中:把数据写到缓冲区,缓冲区通过通道进行传输,到程序中把数据拿出来
- 所以我们说原来的 IO 单向的,现在的缓冲区是双向的,这种传输数据的方式也叫面向缓冲区
- 总结一下:通道只负责连接,缓冲区才负责存储数据
3、缓冲区的数据存取
缓冲区(Buffer):一个用于特定基本数据类型的容器,由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类
3.1、缓冲区的类型
缓冲区(Buffer):在 Java NIO 中负责数据的存取,缓冲区就是数组,用于存储不同类型的数据,根据数据类型的不同(boolean 除外),提供了相应类型的缓冲区
ByteBuffer、ShortBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer、MappedByteBuffer
上述缓冲区管理方式几乎一致,都是通过 allocate() 来获取缓冲区
3.2、缓冲区存取数据的两个核心方法
put() // 存入数据到缓冲区中 // 从缓冲区中读取剩余的所有字节的数据,并将数据存储到目标字节数组 dst 中 // 如果缓冲区中的字节数不足目标字节数组 dst 的长度,则只会读取缓冲区中的所有字节,并将它们存储到目标字节数组中 public ByteBuffer get(byte[] dst); // 从缓冲区中读取 length 个字节的数据 // 并将数据存储到目标字节数组 dst 的 offset 位置开始的位置上 public ByteBuffer get(byte[] dst, int offset, int length);
3.3、缓冲区中的四个核心属性
// 0 <= mark <= position <= limit <= capacity capacity // 容量:最大存储数据的容量,一旦声明不能更改 limit // 界限:可以操作数据的大小,limit 后的数据不能进行读写 position // 位置:正在操作数据的位置 mark // 标记:表示记录当前 position 的位置,可以通过 reset() 恢复到 mark 的位置
3.4、示例代码
public static void test() { String str = "abcde"; // 分配一个指定大小的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); System.out.println("---------allocate---------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 1024 System.out.println(byteBuffer.position()); // 0 // 利用 put() 存入数据到缓冲区中 byteBuffer.put(str.getBytes()); System.out.println("------------put-----------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 1024 System.out.println(byteBuffer.position()); // 5 // 切换到读数据模式 byteBuffer.flip(); System.out.println("-----------flip-----------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 5, limit 表示可以操作数据的大小, 只有 5 个字节的数据给你读, 所以可操作数据大小是 5 System.out.println(byteBuffer.position()); // 0, 读数据要从第 0 个位置开始读 // 利用 get() 读取缓冲区中的数据 byte[] data = new byte[byteBuffer.limit()]; byteBuffer.get(data); System.out.println(new String(data, 0, data.length)); System.out.println("------------get-----------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 5, 可以读取数据的大小依然是 5 个 System.out.println(byteBuffer.position()); // 5, 读完之后位置变到了第 5 个 // rewind() 可重复读, 这个方法调用完后, 又变成了读模式 byteBuffer.rewind(); System.out.println("----------rewind----------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 5 System.out.println(byteBuffer.position()); // 0 // clear() 清空缓冲区, 虽然缓冲区被清空了, 但是缓冲区中的数据依然存在, 只是处于 "被遗忘" 状态 // 解释: 缓冲区中的界限、位置等信息都被置为最初的状态了, 所以你无法再根据这些信息找到原来的数据了, 原来数据就处于 "被遗忘" 状态 byteBuffer.clear(); System.out.println("----------clear-----------"); System.out.println(byteBuffer.capacity()); // 1024 System.out.println(byteBuffer.limit()); // 1024 System.out.println(byteBuffer.position()); // 0 }
public static void test() { String str = "abcde"; ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put(str.getBytes()); // 切换到读数据模式 byteBuffer.flip(); byte[] data = new byte[byteBuffer.limit()]; byteBuffer.get(data, 0, 2); System.out.println(new String(data, 0, 2)); // ab System.out.println(byteBuffer.position()); // 2 // 标记一下当前 position 的位置 2 byteBuffer.mark(); byteBuffer.get(data, 2, 2); System.out.println(new String(data, 2, 2)); // cd System.out.println(byteBuffer.position()); // 4 // reset() 恢复到 mark 的位置 2 byteBuffer.reset(); System.out.println(byteBuffer.position()); // 2 // 判断缓冲区中是否还有剩余数据 if (byteBuffer.hasRemaining()) { // 获取缓冲区中可以操作的数量 System.out.println(byteBuffer.remaining()); // 5 - 2 = 3 } }
4、非直接缓冲区与直接缓冲区
4.1、非直接缓冲区
通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存之中
应用程序和磁盘之间想要传输数据,是没有办法直接进行传输的,操作系统出于安全的考虑,会经过图中几个步骤
- 应用程序想从磁盘中读取一个数据,这时应用程序向操作系统发起一个读请求
首先:磁盘中的数据会被读取到内核地址空间中
然后:会把内核地址空间中的数据拷贝到用户地址空间中(其实就是 JVM 内存中)
最后:再把这个数据读取到应用程序中来 - 应用程序有数据想要写到磁盘中去
首先:把这个数据写入到用户地址空间中去
然后:把数据拷贝到内核地址空间
最后:再把这个数据写入到磁盘中去
4.2、直接缓冲区
通过 allocateDirect() 方法分配缓冲区,将缓冲区建立在物理内存之中
直接用物理内存作为缓冲区,读写数据直接通过物理内存进行
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定
public static void test() { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); // 分配直接缓冲区 System.out.println(byteBuffer.isDirect()); // 判断是直接缓冲区还是非直接缓冲区: true }
5、通道
5.1、介绍
通道(channel):由 java.nio.channels 包定义的,Channel 表示 IO 源与目标打开的连接
Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互
通道用于源节点与目标节点的连接,在 Java NIO 中负责缓冲区中数据的传输,Channel 本身不存储数据,因此需要配合缓冲区进行传输
5.2、主要实现类
java.nio.channels.Channel
- FileChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
5.3、获取通道
Java 针对支持通道的类提供了 getChannel() 方法,通过调用 getChannel() 方法获取通道
- 本地 IO:FileInputStream、FileOutputStream、RandomAccessFile
- 网络 IO:Socket、ServerSocket、DatagramSocket
在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel() 方法
6、通道数据传输和内存映射文件
6.1、使用通道完成文件的复制
// 利用通道完成文件的复制(非直接缓冲区) public static void test() throws Exception { FileInputStream in = new FileInputStream("F:\\test-file\\in.txt"); FileOutputStream out = new FileOutputStream("F:\\test-file\\out.txt"); // 获取通道 FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); // 通道没有办法传输数据, 必须依赖缓冲区, 分配指定大小的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 将通道中的数据存入缓冲区中, inChannel 中的数据读到 byteBuffer 缓冲区中 while (inChannel.read(byteBuffer) != -1) { byteBuffer.flip(); // 切换成读数据模式 outChannel.write(byteBuffer); // 将缓冲区中的数据写入通道 byteBuffer.clear(); // 清空缓冲区 } outChannel.close(); inChannel.close(); out.close(); in.close(); }
6.2、使用直接缓冲区完成文件的复制
// 使用直接缓冲区完成文件的复制(mmap) public static void test() throws Exception { /** * 使用 open 方法来获取通道 * 需要两个参数 * 参数 1: Path 是 JDK 1.7 以后给我们提供的一个类, 代表文件路径 * 参数 2: Option 就是针对这个文件想要做什么样的操作 * -- StandardOpenOption.READ: 读模式 * -- StandardOpenOption.WRITE: 写模式 * -- StandardOpenOption.CREATE: 如果文件不存在就创建, 存在就覆盖 */ FileChannel inChannel = FileChannel.open(Paths.get("F:\\test-file\\in.txt"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("F:\\test-file\\out.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); /** * 内存映射文件 * 这种方式缓冲区是直接建立在物理内存之上的 * 所以我们就不需要通道了 */ MappedByteBuffer inMapped = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMapped = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size()); // 直接对缓冲区进行数据的读写操作 byte[] data = new byte[inMapped.limit()]; inMapped.get(data); // 把数据读取到 data 这个字节数组中去 outMapped.put(data); // 把字节数组中的数据写出去 inChannel.close(); outChannel.close(); }
public static void test() throws Exception { /** * 通道之间的数据传输(零拷贝) * transferFrom * transferTo */ FileChannel inChannel = FileChannel.open(Paths.get("F:\\test-file\\in.txt"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("F:\\test-file\\out.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); inChannel.transferTo(0, inChannel.size(), outChannel); // outChannel.transferFrom(inChannel, 0, inChannel.size()); 或者可以使用这种方式 inChannel.close(); outChannel.close(); }
7、分散读取与聚集写入
分散(Scatter)和聚集(Gather)
- 分散读取(Scattering Reads):从 Channel 中读取的数据 "分散" 到多个 Buffer 中
- 聚集写入(Gathering Writes):将多个 Buffer 中的数据 "聚集" 到 Channel
public static void test4() throws Exception { RandomAccessFile in = new RandomAccessFile("F:\\test-file\\in.txt", "rw"); FileChannel inChannel = in.getChannel(); // 获取通道 // 分配指定大小缓冲区 ByteBuffer buf1 = ByteBuffer.allocate(2); ByteBuffer buf2 = ByteBuffer.allocate(1024); ByteBuffer[] bufs = {buf1, buf2}; // 分散读取 inChannel.read(bufs); // 参数需要一个数组 for (ByteBuffer byteBuffer : bufs) { byteBuffer.flip(); // 切换到读模式 } System.out.println(new String(bufs[0].array(), 0, bufs[0].limit())); // 打印 he System.out.println(new String(bufs[1].array(), 0, bufs[1].limit())); // 打印 llo RandomAccessFile out = new RandomAccessFile("F:\\test-file\\out.txt", "rw"); FileChannel outChannel = out.getChannel(); // 获取通道 // 聚集写入 outChannel.write(bufs); // 把 bufs 里面的几个缓冲区聚集到 outChannel 这个通道中, 也就是到了 out.txt 文件中 outChannel.close(); }
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17469251.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步