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 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。


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 limit−position。

  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 常量作为参数来实现,否则,默认情况下字节顺序仍为大端字节顺序。

java 复制代码
// 获取当前 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 的非直接缓冲区。

直接缓冲区
  • 直接缓冲区使用了操作系统 的内存,通过 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,并映射到指定文件的指定区域:

java 复制代码
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 读取的数据分散(写入)到多个缓冲区。这种操作可以在读取数据时将其分散到不同的缓冲区,有助于处理结构化数据。

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

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

java 复制代码
// 分散读取数据到多个缓冲区

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

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

java 复制代码
// 聚集数据从多个缓冲区写入到 Channel

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

ByteBuffer[] responseBuffers = {headerResponse, bodyResponse};

long bytesWritten = socketChannel.write(responseBuffers);
相关推荐
SWAGGY..39 分钟前
Linux系统编程:(十三)环境变量
java·linux·算法
程序员黑豆1 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程
道友可好1 小时前
AI 测试全绿,代码却是错的
前端·人工智能·后端
布朗克1681 小时前
34 JVM深入理解
java·jvm
Flittly1 小时前
【AgentScope Java新手村系列】(4)结构化输出
java·spring boot·spring·ai
techdashen1 小时前
Rust 基础设施团队 2025 Q4 回顾与 2026 Q1 计划
开发语言·后端·rust
何以解忧,唯有..1 小时前
Python 中的继承机制:从基础到高级用法详解
java·开发语言·python
Yiyaoshujuku1 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构
神奇小汤圆1 小时前
互联网大厂精选面试八股文(附2026最新Java+AI高频题)| 建议收藏
后端
春天花会开1312 小时前
影像上传前置机网络架构设计模板(含VPN)
后端·架构