7

面试官:Java NIO 的 Buffer 缓冲区,你了解多少?

 2 years ago
source link: https://www.cxyxiaowu.com/20566.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引言

缓冲区是一个用于特定基本类型的容器。由java.nio 包定义,所有缓冲区都是 Buffer 抽象类的子类。

3fc6fde9-8969-4ce4-baa1-372497706de9.jpg

Java NIO 中的 Buffer ,主要用于与NIO 通道进行交互。数据从通道存入缓冲区,从缓冲区取出到通道中。

一、创建缓冲区

缓冲区的本质是 数组 ,用于存储不同类型的数据,根据数据类型(boolean 除外),提供了相应类型的缓冲区,如ByteBuffer、IntBuffer等。这些缓冲区的管理方式都是类似的,都是通过 allocate() 方法指定容量并创建缓冲区。

// 创建一个 1 KB 大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);

一般情况下,我们通过 allocate() 方法创建缓冲区,但是在需要高性能的地方,有时候往往需要使用 allocateDirect() 方法。

allocate() 创建非直接缓冲区,allocateDirect() 创建直接缓冲区。

二、缓冲区的四个核心属性

缓冲区的本质实际上是一个数组,最常用的ByteBuffer,本身就是一个 byte[] 数组,根据数据读取的场景,设计者为Buffer 设置了四个核心属性,定义在 Buffer 抽象类中:

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

缓冲区的操作实际上是借由这四个 int 标记来完成的,可以理解为抽象的指针。它们的关系如下:

0 <= mark <= position <= limit <= capacity

  • position 表示位置,表示当前程序正在操作的数据的下一个索引值。
  • mark 表示标记,通过 mark() 方法,记录当前数据的索引。可以通过 reset() 重新找到 mark 所指向的数据。
  • limit 界限,表示缓冲区中可以操作数据的大小,limit 后的数据不能进行读写。
  • capacity 缓冲区容量,因为缓冲区本身就是数组,因此一旦声明不能改变该值。

2.1 初始的指针状态

假设我们声明了一个 capacity 为 5 的字节缓冲区:

ByteBuffer buf = ByteBuffer.allocate(5);

那么,缓冲区的初始状态就是如下图所示:

904f7db8-0758-45d0-9e24-8aedcaa37340.jpg

2.2 当缓冲区中有数据的状态

由于缓冲区独特的构造,在读和写的时候,limit 与 position 指针是有一定区别的。

// 写模式
byteBuffer.put("Tom".getBytes());

3f9a9e9b-ebe5-4a9c-99da-4999648b799b.jpg

// 读模式
byteBuffer.get();

ae25994f-df32-410f-8a20-637d4386f884.jpg

三、缓冲区的核心方法

3.1 存取数据

缓冲区既然作为数据的容器,必然涉及到数据的存取操作,但要注意,存和取操作不可以连续执行,两个动作之间需要有一个 “翻转” 的操作。

  • put() 方法将数据放入到缓冲区中;get() 方法从缓冲区中取出数据。

3.2 flip()翻转、rewind()倒带、clear()清空

  • flip() : 翻转,将缓冲区进行读写切换。
  • rewind() : 倒带,可以将 position 和 limit 回退到上一次操作前。
  • clear() :清空缓冲区,官方说明是“clears the buffer”,但详细解释是将 position 和 limit 恢复“出厂设置”,并丢弃 mark。注意,缓冲区中的数据并非清空,只是将两个指针重置,数据处在一种“被遗忘”状态,如果进行 get()操作依然可以取出。同时,clear 执行之后的缓冲区无法通过 rewind() 回退指针。

3.3 mark()标记、reset()定位

  • mark()方法可以记录当前 position 的位置,并可以通过 reset() 方法恢复到 mark()

3.4 hasRemaining()是否有未读数据、remaining()获取未读数据数量

  • hasRemaining() 用于判断读模式下的 Buffer 中是否还有未读数据;
  • remaining() 方法可以返回剩余可操作的元素个数。其值与 limit – position 的差值相等。

3.5 示例程序

从一开始创建一个 Buffer 开始,通过存入、读取数据来观察各个属性:capacity、limit、position、mark 等的变化。

// 分配 1 KB 大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("=============allocate()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

a147b5ec-c833-4ccb-a61e-65ca8c8e8a90.jpg

System.out.println("=============put()===========");
String name = "abcde";
byteBuffer.put(name.getBytes());
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

f28435c6-6e96-45ad-a8f1-6617f777fd22.jpg

System.out.println("============flip()===========");
byteBuffer.flip();
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

6ac1b0db-b797-4f80-afe1-747c9b7d8bbc.jpg

System.out.println("============get()===========");
byte[] dst = new byte[byteBuffer.limit()];
byteBuffer.get(dst);
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());
System.out.println(new String(dst));

ee27fea0-0a59-4b72-8525-a143193cfc62.jpg

System.out.println("============rewind()===========");
byteBuffer.rewind();
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

5b91b66e-3de9-484e-b2b2-013415aa927b.jpg

System.out.println("============clear()===========");
byteBuffer.clear();
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

4e3f6347-3eb6-45b2-89a3-1c8ce38ddd6f.jpg

标记、定位标记:

ByteBuffer buf = ByteBuffer.allocate(5);
buf.put("abcde".getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2); // get(byte[] dst, int offset, int length)
System.out.println("第一次取出结果:" + new String(dst));
System.out.println("position:" + buf.position());
// mark()标记
buf.mark();
buf.get(dst, 2, 2); // get(byte[] dst, int offset, int length)
System.out.println("第二次取出结果:" + new String(dst));
System.out.println("position:" + buf.position());
// 恢复到mark
buf.reset();
System.out.println("reset 恢复到 mark 位置");
System.out.println("position:" + buf.position());

45c80cdd-7ae2-4c28-a809-7532f79a6889.jpg

查询剩余数据:

// remaining() 获取缓冲区中还可以操作的数量
if (buf.hasRemaining()) {
 System.out.println(buf.remaining());
 System.out.println("limit - position = " + (buf.limit() - buf.position()));
}

6e495ca4-3c25-4bd6-8afe-66cefaab9c90.jpg

四、直接缓冲区与非直接缓冲区

字节缓冲区要么是 直接缓冲区,要么是 非直接缓冲区。

非直接缓冲区属于常规操作,传统的 IO 流和 allocate() 方法分配的缓冲区都是非直接缓冲区,建立在 JVM 内存中。这种常规的非直接缓冲区会将内核地址空间中的内容拷贝到用户地址空间(中间缓冲区)后再由程序进行读或写操作,换句话说,磁盘上的文件在与应用程序交互的过程中会在两个缓存中来回进行复制拷贝。

而直接缓冲区绝大多数情况用于显著提升性能,缓冲区直接建立在物理内存(相对于JVM 的内存空间)中,省去了在两个存储空间中来回复制的操作,可以通过调用 ByteBuffer 的 allocateDirect() 工厂方法来创建。直接缓冲区中的内容可以驻留在常规的垃圾回收堆之外,因此它们对应用程序的内存需求量造成的影响可能并不明显。另外,直接缓冲区还可以通过 FileChannel 的 map() 方法将文件直接映射到内存中来创建,该方法将返回 MappedByteBuffer 。

直接或非直接缓冲区只针对字节缓冲区而言。字节缓冲区是那种类型可以通过 isDirect() 方法来判断。

b64510e2-e956-40f8-be1b-b18fb78808c9.jpg

注意!!!直接缓冲区性能虽然好,但是缓冲区直接建立在物理内存中,无法由 GC来释放,可控性差,同时分配和销毁成本很高!在对性能不是特别依赖的场景不建议使用!

a49d3ce0-1279-4851-bd28-25253c1482ed.jpg

来源:圣斗士Morty

来源:blog.csdn.net/u014745069/article/details/99709696


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK