2、Java NIO

参考:什么是 NIO ?6000 字详解 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 流是单向的

image

NIO:面向缓冲区

  • 只要是 IO,那么就是为了完成数据传输的,即便用 NIO,它也是为了数据传输,所以你要想完成数据传输,你也得建立一个用于传输数据的通道
    这个通道你不能把它理解为之前的水流了,但是你可以把它理解为铁路
    铁路本身是不能完成运输的,铁路要想完成运输必须依赖火车,说白了这个通道就是为了连接源地点和目标地点
    注意:通道本身不能传输数据,要想传输数据必须要有缓冲区,这个缓冲区你就可以完全把它理解为火车
  • 你现在想把程序中的数据写到文件中:把数据都写到缓冲区,然后缓冲区通过通道进行传输,最后再把数据从缓冲区拿出来写到文件中
  • 你想把文件中的数据传数到程序中:把数据写到缓冲区,缓冲区通过通道进行传输,到程序中把数据拿出来
  • 所以我们说原来的 IO 单向的,现在的缓冲区是双向的,这种传输数据的方式也叫面向缓冲区
  • 总结一下:通道只负责连接,缓冲区才负责存储数据

image

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 的位置

image

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 内存中)
    最后:再把这个数据读取到应用程序中来
  • 应用程序有数据想要写到磁盘中去
    首先:把这个数据写入到用户地址空间中去
    然后:把数据拷贝到内核地址空间
    最后:再把这个数据写入到磁盘中去

image

4.2、直接缓冲区

通过 allocateDirect() 方法分配缓冲区,将缓冲区建立在物理内存之中
直接用物理内存作为缓冲区,读写数据直接通过物理内存进行
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定

public static void test() {
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); // 分配直接缓冲区
    System.out.println(byteBuffer.isDirect());               // 判断是直接缓冲区还是非直接缓冲区: true
}

image

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

image

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();
}
posted @ 2023-06-09 15:02  lidongdongdong~  阅读(23)  评论(0编辑  收藏  举报