Java 入门指南:Java NIO —— Buffer(缓冲区)

NIO 的引入

在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。

为了解决这个问题,在 Java1.4 版本引入了 NIO(New I/O or Non-Blocking I/O)java.nio。提供了一种基于缓冲区、选择器和非阻塞 IO 模型的 IO 处理方式。相比于之前的 BIO 模型,NIO 可以实现更高的并发、更低的延迟以及更少的资源消耗。

I/O 包和 NIO 已经很好地集成了,java.io 也已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

![[BIO vs NIO.png]]
Java NIO 概要介绍:初识 Java NIO

使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。

Buffer

在 NIO(New Input/Output)模型中,Buffer 是一个重要的概念,与数据打交道,用于在内存中存储数据通过 Channel 将数据传输到 Buffer 缓冲区中,并在缓冲区内进行数据的读写操作

Buffer 本质上是一个数组,可以存储多个相同类型的基本数据类型,如 byte、short、int、long、float、double 等。Buffer 封装了内部的数组,并提供了一些操作该数组的方法。

NIO 提供了多种 Buffer 类型,如

  • ByteBuffer(最常用的 Buffer 类)
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每种 Buffer 类型对应着不同的基本数据类型,用于存储不同类型的数据。

核心变量

Buffer 除了存储数据外,还保存着四个关键属性:

  1. capacity:表示 Buffer 的容量,即可以存储的最大数据量,容量在创建 Buffer 时就确定,之后不可修改。

  2. position:表示当前 Buffer 中已经处理的数据位置,初始值为 0,必须手动设置。每当进行读写操作时,Position 会自动由相应的 get()put() 函数更新,向前移动

  3. limit:表示 Buffer 中可供访问的数据的最大位置(上界),初始值为 capacity,可以手动设置为 position 或其他值。

  4. mark:表示一个备忘记录,用于在某个特定位置设置 mark,然后在之后的某个时间点通过调用 reset() 方法恢复到该 position。

在进行数据读写操作时,一般需要调用 put() 方法将数据写入 Buffer 中,或者调用 get() 方法从 Buffer 中读取数据。

对于写入操作,由于 position 属性的存在,保证了写入的数据不会覆盖已有的数据;对于读取操作,由于 position 属性的存在,保证了读取的数据不会超出可访问的数据范围。

使用 Buffer 进行数据读写时,需要注意处理好 position 和 limit 属性的值,以及不同类型的 Buffer 之间的数据转换问题。Buffer 并不是线程安全的,使用时需要注意线程同步的问题

常用方法

  1. put():向 Buffer 中写入数据。put() 方法有多个重载形式,它们可以将不同类型的数据写入 Buffer 中。例如 put(byte b)、putInt(int i)、putFloat(float f) 等。

    • put(type value):将指定类型的数据写入到 Buffer 中,position 会自动向前移动。

    • put(byte[] array):将 byte 数组从当前 position 处写入 Buffer,同时会增加 position 的值。

    • put(ByteBuffer src):将 src 中的剩余字节写入 Buffer,同时会增加 position 的值。

  2. get():从 Buffer 中读取数据。get() 方法也有多个重载形式,根据不同的数据类型可以选择对应的 get() 方法进行读取。例如,getInt()、getFloat()、getChar() 等。

    • get():从当前 position 处读取一个字节,并将 position 向前移动。

    • get(byte[] array):将从当前 position 处开始的字节序列读入给定的 byte 数组中,并增加 position 的值。

    • get(ByteBuffer dst):将从当前 position 处开始的字节序列读入给定的 ByteBuffer 中,并增加 position 和 dst 的 position 值。

  3. flip():设置 Buffer 的 limit 属性 为 当前的 position,然后将 position 属性设置为 0。在写入数据后,调用 flip() 方法可以将 Buffer 切换到读模式。

  4. rewind():将 position 属性设置为 0,不改变 limit 属性,可以重复读取 Buffer 中的数据。

  5. clear():将 Buffer 清空,position 和 limit 属性设置为初始值。可以重复写入 Buffer 中的数据。

  6. compact():将 position 属性设置为 Buffer 中未处理数据的下一个位置,将未读取的数据移到缓冲区头部,以便更多数据写入。limit 属性则表示缓冲区尾部未处理空间的末尾位置。

  7. mark():用于设置一个备忘位置

  8. reset():用于恢复到 mark() 所标记的位置。

  9. capacity():返回 Buffer 的容量,即可以存储的最大数据量。

  10. position():返回当前 Buffer 中的位置(position)。

  11. position(int newPostition):设置 Buffer 的 Position

  12. limit():返回 Buffer 的上界(limit),表示 Buffer 中可供访问的数据的最大位置。

  13. limit(int newLimit):设置 Buffer 的 limit

  14. remaining():返回剩余可读取或可写入的元素数量,即 l i m i t − p o s i t i o n limit - position limitposition

  15. hasRemaining():检查是否还有剩余可读取或可写入的元素。

  16. isReadOnly():检查 Buffer 是否为只读缓冲区。

  17. array():返回 Buffer 所支持的数组,如果 Buffer 不支持数组,则抛出 UnsupportedOperationException 异常。

  18. duplicate():创建一个与原 Buffer 共享相同数据的新 Buffer。

  19. slice():创建一个新 Buffer,与原 Buffer 共享相同数据,但通过修改其 position、limit 和 mark 属性来表示一个更小的数据集合。

  20. compact():将未读取的数据移到 Buffer 的开头,同时将 position 设置为未读取数据的结尾,便于继续写入数据。

  21. wrap(byte[] array, int offset, int length):将一个字节数组或指定范围的字节数组包装成一个 ByteBuffer 对象。

    offset:包装的起始位置。length:包装的长度。二者可以为空

    使用 ByteBuffer.wrap() 方法可以方便地将字节数组转换为 ByteBuffer 对象,从而可以进行更方便的读取和写入操作。需要注意的是,通过 wrap() 方法包装的 ByteBuffer 对象和原始的字节数组共享内存空间,对其中一个的修改会影响到另一个。

ByteBuffer

ByteBuffer 是 Java NIO 中的一个缓冲区(Buffer)类,用于在内存中存储字节数据。它是一个抽象类,并且是 Buffer 类最常用的子类,提供了操作字节数据的方法。

获取和设置当前字节顺序

ByteBuffer 类中的 order() 方法用于获取或设置字节顺序(Byte Order),字节顺序指的是在多字节数据存储中,高字节放在哪个位置。在 Java NIO 中,ByteBuffer 中的数据存储都是以大端字节顺序(Big Endian)进行存储的,即高位字节存放在低位地址处,而低位字节存放在高位地址处

如果需要改变字节顺序,在 ByteBuffer 实例化后,可以通过调用 order() 方法设置字节顺序。如果需要将其切换到小端字节顺序(Little Endian),可以通过传入 ByteOrder.LITTLE_ENDIAN 常量作为参数来实现,否则,默认情况下字节顺序仍为大端字节顺序。

// 获取当前 ByteBuffer 的字节序 
ByteOrder order = byteBuffer.order(); 

// 设置 ByteBuffer 的字节序为小端字节序 
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

直接缓冲区与非直接缓冲区

在 Java NIO 中,缓冲区(Buffer)是一块连续的内存区域,用于在 Java 程序和底层 I/O 之间传输数据。Java NIO 提供了两种类型的缓冲区:直接缓冲区(Direct Buffer)和非直接缓冲区(Non-direct Buffer)。

  • 非直接缓冲区存储在 JVM 内部,数据需要从应用程序(Java)复制到非直接缓冲区,再复制到内核缓冲区,最后发送到设备(磁盘/网络)。

  • 对于直接缓冲区,数据可以直接从应用程序(Java)复制到内核缓冲区,无需经过 JVM 的非直接缓冲区。

![[HeapByteBuffer&DirectByteBuffer.png]]

直接缓冲区
  • 直接缓冲区使用了操作系统的内存,通过 ByteBuffer.allocateDirect(int capatity) 方法创建。

  • 直接缓冲区的创建和销毁比较慢,但在 I/O 操作中的性能一般较好,特别适合大量的数据传输。

  • 由于直接缓冲区使用了堆外内存,因此对于频繁的 I/O 操作,可以减少数据复制的过程,提高读写效率。

  • 直接缓冲区的内存分配和销毁不受 Java 堆大小的影响,但是由于使用了本地内存,可能会导致内存消耗较大。因此,在使用直接缓冲区时,需要谨慎使用和及时释放。

非直接缓冲区
  • 非直接缓冲区使用了 JVM 堆 内存,通过 ByteBuffer.allocate(int capatity) 或其他分配方法创建。

  • 非直接缓冲区的创建和销毁速度相对较快,因为它是在 Java 堆上分配的内存。

  • 非直接缓冲区的 I/O 性能相对较差,因为在进行 I/O 操作时,还需要进行数据复制,增加了数据复制的开销。

  • 由于使用了 Java 堆内存,因此受到堆大小的限制,当 Java 堆内存较小或已用内存较大时,可能会导致内存不足或频繁的垃圾回收。

MappedByteBuffer

MappedByteBuffer 是 Java NIO 中的一个特殊类型的缓冲区,用于表示一个内存映射文件,将文件的一部分或全部映射到内存中。它与文件 NIO 通道(FileChannel)相关联,并且只能通过 Channel 的 文件通道(FileChannel)创建。

MappedByteBuffer 可以让文件直接在内存(堆外内存)中进行修改,通过直接操作内存来实现对文件的读写,而不需要将文件从磁盘上复制到一个缓冲区中,使得操作文件的 I/O 性能得到提高。

特点

MappedByteBuffer 有以下几个特点:

  1. 只能通过 FileChannel 创建。

  2. 需要将文件映射到内存中才能进行数据的读写。

  3. 映射区域的大小不能超过 Integer.MAX_VALUE

  4. 修改缓冲区中的数据也会修改文件中的数据。

构造方法

MappedByteBuffer 可以通过 FileChannel 的 map 方法来创建,该方法返回一个新的 MappedByteBuffer,并映射到指定文件的指定区域:

FileChannel.map(FileChannel.MapMode.mode, 
				int position,
				int size)
  • mode:表示是只读模式(READ_ONLY)、读写模式(READ_WRITE)或专用模式(PRIVATE)。

  • position:表示从文件的哪个位置开始映射。

  • size:表示映射到内存的字节数,即缓冲区的容量。

映射到的缓冲区可以进行修改,并且对文件进行修改,这种修改是直接写入到文件,因此修改后的内容将立即反映到文件中。不过需要注意的是,如果写入的数据超过了映射区域的大小,则会抛出异常。

force()

MappedByteBuffer.force() 方法将缓冲区中的修改刷新到磁盘上,保持文件和内存的一致性。

Scatter 和 Gather

Java IO 中的 ScatterGather 是一种 I/O 模式,用于在网络或磁盘 I/O 操作时改善性能。它们在 NIO(New IO)中引入,并在 Java 1.4 中加入。ScatterGather 模式通过将 I/O 操作中的散乱数据块收集到一个连续的缓冲区中(Gather),或者将一个连续的数据块分散到不同的缓冲区中(Scatter),来减少数据挪动和复制的次数,从而提高了性能。

Scatter

Scatter 模式下,它将从一个 Channel 读取的数据分散(写入)到多个缓冲区。这种操作可以在读取数据时将其分散到不同的缓冲区,有助于处理结构化数据。

例如,我们可以将消息头、消息体和消息尾分别写入不同的缓冲区。

这种模式常用于将数据分发到多个不同的缓冲区,并且数据也可以从多个通道读入到一个缓冲区中。

// 分散读取数据到多个缓冲区

ByteBuffer headerBuffer = ByteBuffer.allocate(128);
ByteBuffer bodyBuffer = ByteBuffer.allocate(1024);

ByteBuffer[] buffers = {headerBuffer, bodyBuffer};

long bytesRead = socketChannel.read(buffers);

// 输出缓冲区数据
headerBuffer.flip();
while (headerBuffer.hasRemaining()) {
    System.out.print((char) headerBuffer.get());
}

System.out.println();

bodyBuffer.flip();
while (bodyBuffer.hasRemaining()) {
    System.out.print((char) bodyBuffer.get());
}
Gather

Gather 模式下,与 Scatter 相反,它将多个缓冲区中的数据聚集(读取)并写入到一个 Channel。这种操作允许我们在发送数据时从多个缓冲区中聚集数据。

例如,我们可以将消息头、消息体和消息尾从不同的缓冲区中聚集到一起并写入到同一个 Channel

这种模式常用于自动化多路复用和数据重新组合。

// 聚集数据从多个缓冲区写入到 Channel

ByteBuffer headerResponse = ByteBuffer.wrap("Header Response".getBytes());
ByteBuffer bodyResponse = ByteBuffer.wrap("Body Response".getBytes());

ByteBuffer[] responseBuffers = {headerResponse, bodyResponse};

long bytesWritten = socketChannel.write(responseBuffers);
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值