【源码解析】Java NIO 包中的 Buffer

王柳  论坛元老 | 2025-1-14 12:05:05 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1024|帖子 1024|积分 3072


1. 媒介

Buffer 是 JDK 1.4 引入的 NIO 包下面的一个核心类,重要是为了提供一种更高效、更灵活的方式来进行 I/O 操作。
对于传统的 IO ,往往会涉及到多次内存的复制,比如从内核态复制到用户态,再赋值到应用步调缓冲区,Buffer 就提供了一种直接映射内存地区的可能,淘汰这些数据的复制,提高查询的效率。
除别的,Buffer 在 NIO 中用来存储数据的容器,Channel 通过 Buffer 进行读写操作,从而实现高效的 NIO,也就是非阻塞 IO。
关于 Buffer 就简单先容这么多,其实没有 Buffer 之前,传统的 IO 操作通过输入输出流来处理惩罚,一次只能处理惩罚一个字节,性能就不消多说了,是比较低的,有了 Buffer 之后,一次就能读取一批的数据到 Buffer 中来处理惩罚,性能从而能大大提高,比如说下面我们要读取一个文件的时候,可以使用 FileChannel 配合 Buffer 来进行读取
  1. public class FileReaderTest {
  2.     public static void main(String[] args) {
  3.         String filePath = "example.txt";
  4.         try (RandomAccessFile accessFile = new RandomAccessFile(filePath, "r");
  5.              FileChannel fileChannel = accessFile.getChannel()) {
  6.             ByteBuffer buffer = ByteBuffer.allocate(1024);
  7.             // 读取数据到 buffer 缓冲区中
  8.             while (fileChannel.read(buffer) > 0) {
  9.                 // 切换读模式
  10.                 buffer.flip();
  11.                 while (buffer.hasRemaining()) {
  12.                     // 获取剩余字节
  13.                     System.out.print((char) buffer.get());
  14.                 }
  15.                 // 清空缓冲区,准备下一次读取
  16.                 buffer.clear();
  17.             }
  18.         } catch (IOException e) {
  19.             e.printStackTrace();
  20.         }
  21.     }
  22. }
复制代码
输出如下:


2. 概述

上面就先容了下 Buffer 的作用和为什么要引入 Buffer,那么既然 Buffer 可以一次性处理惩罚那么多的数据,那这些数据怎么存储的呢?既然 Buffer 可写可读,那么切换模式的时候是怎么确保上一次没有读完的数据不会被覆盖的?…
要表明上面的问题,就要去看 Buffer 的源码,那再看详细的源码逻辑之前,我们先看内里的属性。

3. 属性

   private int mark = -1
  mark 是 Buffer 中的一个标志位,用来标志原来的 position 的位置。


  • 当 Buffer 中必要行止理惩罚其他位置的数据的时候,可以使用 mark 先标志一下原来的位置,等处理惩罚完再回到原来的 position 继承处理惩罚
  • 如果调用者想要多此读取同一部分的数据,可以使用 mark 来标志原来的 position,读完之后再设置 position = mark,就可以重复读取了
下面来看下 mark 的用法,我们用 mark 来实现对 Buffer 的一段数据重复读:
  1. public class MarkTest {
  2.     public static void main(String[] args) {
  3.         IntBuffer buffer = IntBuffer.allocate(10);
  4.         buffer.put(1);
  5.         buffer.put(2);
  6.         buffer.put(3);
  7.         buffer.put(4);
  8.         // 切换读模式
  9.         buffer.flip();
  10.         // 做下标记
  11.         buffer.mark();
  12.         System.out.println(buffer.get());  // 1
  13.         System.out.println(buffer.get());  // 2
  14.         // 回到标记的位置,就是下标 0 的位置
  15.         buffer.reset();
  16.         System.out.println(buffer.get()); // 1
  17.         System.out.println(buffer.get()); // 2
  18.     }
  19. }
复制代码
可以看到,上面再 buffer 切换读模式之后,调用 mark 做了标志,然后再调用 reset 就可以回到标志的位置。这里提前剧透下,所谓的标志就是 position,一开始切换到读模式之后 position = 0,所以 mark = position = 0,调用 reset 之后 position 重新设置为 0,继承从头开始读取。

   private int limit;
  limit 是表示 Buffer 差别操作模式的上限。


  • 在 Buffer 写模式情况下,可写元素的上限就是团体容量,也就是说 在可写模式下 limit = capacity,这个 capacity 就是 Buffer 的容量上限。
  • 在 Buffer 读模式情况下,可写元素的上限就是在可写模式下的边界,也就是说,当 Buffer 切换到读模式之后必要设置 limit = position 来标志写模式下写入的最后一个元素的位置。

   private int capacity;
  capacity 表示 Buffer 的容量,也就是详细可以容纳多少个元素

   private int position = 0;
  position 表示 Buffer 的下一个可操作的元素的位置,由于 Buffer 有两种模式,读模式和写模式,那么在两种模式下这个 position 的定义有所差别


  • 写模式下, position 表示下一个可写入的位置
  • 读模式下,position 表示下一个可读的位置

   long address;
  address 表示 Buffer 地址,Buffer 的实现类ByteBuffer 有三个子类,分别是 DirectByteBuffer、HeapByteBuffer、MappedByteBuffer


  • DirectByteBuffer 是直接内存,也就是堆外内存
  • MappedBuffer 通过 mmap 的方式将文件中的内容映射到内存中,也能算是一个堆外内存了
  • HeapByteBuffer 就不消多说了,看名字就知道是堆内存,由 JVM 分配管理的
对于 HeapByteBuffer 这种 JVM 分配管理的 Buffer,内部可以用一个数组来存储数据。但是对于 DirectByteBuffer 和 MappedBuffer 这种不是 JVM 管理采取的,就不能用一个数组来管理了,这时候就必要直接对地址操作,所以这个 address 就是记录这部分内存的起始地址。
好了,看了上面几个参数,现在给一个大概的标志图。

上面就是这几个参数在写模式下面的大概标志图了,那么读模式呢?比如上面下标 0-4 写入了数据,此时切换读模式,那么读模式就是如下图所示。

那么为什么会这样变革呢?


  • 上面图中写入 5 个元素之后,position 指向了下标 5 的位置,因为 position 指向的是下一个可写的元素,所以切换后 limit 自然就酿成了 position,也就是下标 5 的位置

4. 方法

4.1 构造器

Buffer 是最底层的抽象类,所以并没有进行进一步的封装,也就是说 Buffer 的构造器必要指定 mark、pos、limit、cap 四个属性。
  1. /**
  2. * Creates a new buffer with the given mark, position, limit, and capacity, after checking invariants.
  3. * @param mark
  4. * @param pos
  5. * @param lim
  6. * @param cap
  7. */
  8. Buffer(int mark, int pos, int lim, int cap) {       // package-private
  9.     if (cap < 0)
  10.         throw new IllegalArgumentException("Negative capacity: " + cap);
  11.     // 1.设置capacity
  12.     this.capacity = cap;
  13.     // 2.设置limit
  14.     limit(lim);
  15.     // 3.设置position
  16.     position(pos);
  17.     if (mark >= 0) {
  18.         if (mark > pos)
  19.             throw new IllegalArgumentException("mark > position: ("
  20.                                                + mark + " > " + pos + ")");
  21.         // 4.设置mark
  22.         this.mark = mark;
  23.     }
  24. }
复制代码
上面构造器的源码就是在设置这几个属性,那么来看下 limit 的逻辑。
  1. /**
  2. * Sets this buffer's limit.  If the position is larger than the new limit
  3. * then it is set to the new limit.  If the mark is defined and larger than
  4. * the new limit then it is discarded.
  5. * 设置limit
  6. *
  7. * @param  newLimit
  8. *         The new limit value; must be non-negative
  9. *         and no larger than this buffer's capacity
  10. *
  11. * @return  This buffer
  12. *
  13. * @throws  IllegalArgumentException
  14. *          If the preconditions on <tt>newLimit</tt> do not hold
  15. */
  16. public final Buffer limit(int newLimit) {
  17.     if ((newLimit > capacity) || (newLimit < 0))
  18.         throw new IllegalArgumentException();
  19.     // 设置limit
  20.     limit = newLimit;
  21.     // 如果position比新的limit要大,就需要更新position到最新的newLimit
  22.     if (position > newLimit) position = newLimit;
  23.     // 如果mark比新的limit要大,就重置mark
  24.     if (mark > newLimit) mark = -1;
  25.     return this;
  26. }
复制代码
设置 limit 的时候,我们之前就知道了 limit 就是用来标志 position 的,如果 position 比新设置的 limit 大,那么更新 position 为最新的 limit。


  • 在写模式下,其实 limit 就是容量长度
  • 在读模式下,limit 就是写模式下的 position
如果 position 比新设置的 limit 大,比如切换写模式之后重新设置 limit 值,这时候就得重新设置 position,别写越界了。
下面如果 mark 比新的 limit 要大,就重置 mark。照旧一样的逻辑,mark 标志的是 position 的位置,如果原来 position 都比 limit 大了,那么就阐明限制变小了,这时候设置 mark = -1。
反之就是 position 和 mark 都比 limit 小,其实也不影响写入和读取,所以不消管。
然后下面就是设置 position 的逻辑。
  1. public final Buffer position(int newPosition) {
  2.     if ((newPosition > limit) || (newPosition < 0))
  3.         throw createPositionException(newPosition);
  4.     // 如果mark比新的newPosition要大,就重置下
  5.     if (mark > newPosition) mark = -1;
  6.     position = newPosition;
  7.     return this;
  8. }
复制代码
设置 position 的时候,必要判断不能比 limit 大,同时要设置下 mark,如果 mark 比新的 newPosition 要大,就阐明 position 指针往左移动了,这时候 mark 标志的就是无效数据了,所以设置为 -1,看下面的图。

上面图中 limit 标志的就是无用的位置了。因为 newPosition 指向下标 3,在写模式下会从下标 3 继承写入,所以这时候 limit 位置的会被覆盖,所以说 limit 就是一个无效数据了,下面照旧看一个例子吧。
  1. public static void bufferTest(){
  2.      ByteBuffer buffer = ByteBuffer.allocate(10);
  3.      buffer.put((byte) 1);
  4.      buffer.put((byte) 2);
  5.      buffer.put((byte) 3);
  6.      buffer.put((byte) 4);
  7.      buffer.put((byte) 5);
  8.      // 做下标记
  9.      buffer.mark();
  10.      buffer.position(3);
  11.      buffer.put((byte) 6); // 1 2 3 6 5
  12.      // 回到标记的位置
  13.      buffer.reset(); // 抛出异常
  14. }
复制代码
终极会抛出异常:

就是因为 mark 内里被重新设置了 -1,上面例子本意是重新设置 position 然后覆盖前面写过的 4,但是由于 mark 被重新设置为 -1 了,所以终极调用 reset 就会抛异常。

4.2 flip()

上面说过,Buffer 分为两种模式:读模式和写模式,读模式就是专门读取的,看源码:
  1. public final Buffer flip() {
  2.     // limit 设置成写模式下的 position,读模式的范围就是 [0, position)
  3.     limit = position;
  4.     // 读模式下 position 设置为 0,这样就可以从头开始读取 Buffer 中的数据了
  5.     position = 0;
  6.     // mark 重置为 -1
  7.     mark = -1;
  8.     return this;
  9. }
复制代码

切换为读模式之后,由于写模式下写入了下标 0 - 4,所以读模式下会设置 limit = 5,表示读模式只能读到 5 的位置,position 读指针重新设置为 0,这样就可以从头开始读取 Buffer 中的数据了,mark 重置为 -1。

4.3 clear()

有读模式,就有写模式,写模式顾名思义就是继承往内里写入数据,但是我们这里只是最底层的抽象逻辑,所以和上面的 flip 一样,只是调整几个参数。
  1. /**
  2. * 切换写模式,假设现在数组指针如下:                    capacity
  3. * mark                position                     limit
  4. *  -1   0    1   2       3      4    5    6    7     8
  5. * 切换之后:
  6. *                                                      capacity
  7. * mark  position                                        limit
  8. *  -1      0       1    2     3      4    5    6    7     8
  9. *
  10. * 但是上面的转换有一个问题,如果转换之前我们并没有读完数据,也就是说 [position, limit) 里面的数据还没有读取
  11. * 这时候切换 position 为 0,后续写入不久覆盖了吗
  12. * 所以针对这种情况,我们就需要把 [position, limit) 的数据拷贝到前面,然后再移动数据
  13. *                                                       capacity
  14. * mark           不可覆盖             position    可覆盖    limit
  15. *  -1    0     1    2     3      4      5        6    7     8
  16. *
  17. * 由于 Buffer 是顶层的接口,所以上面的移动就交给了子类来实现,比如 HeapByteBuffer 的 compact
  18. *
  19. * @return  This buffer
  20. */
  21. public final Buffer clear() {
  22.     // 设置为 0,从 0 开始进行写入数据
  23.     position = 0;
  24.     // 重新设置 limit
  25.     limit = capacity;
  26.     // 重新设置 mark
  27.     mark = -1;
  28.     return this;
  29. }
复制代码


4.4 rewind()

rewind() 方法是 Java NIO Buffer 类中的一个重要方法,它的重要作用是重置 position,同时丢弃 mark。
在读取大概写入操作之前,可以调用这个方法回到初始状态,重新处理惩罚数据。

  • 重新读取数据

    • 在读模式情况下,当读取一部分数据之后可以调用这个方法重新从头开始读取数据

  • 重新写入数据

    • 在写模式情况下,当写入一部分数据之后可以调用这个方法重新从头开始写入数据

下面就是这个方法的源码,也就是重新设置 position 和 mark 标志。
  1. public final Buffer rewind() {
  2.     // 重置 position 和 mark
  3.     position = 0;
  4.     mark = -1;
  5.     return this;
  6. }
复制代码
下面有一个例子:
  1. public static void rewindTest(){
  2.     ByteBuffer buffer = ByteBuffer.allocate(10);
  3.     buffer.put((byte) 0);
  4.     buffer.put((byte) 1);
  5.     buffer.put((byte) 2);
  6.     buffer.put((byte) 3);
  7.     buffer.put((byte) 4);
  8.     // 从头开始写入数据
  9.     buffer.rewind();
  10.     buffer.put((byte) -1);
  11.     buffer.put((byte) -2);
  12.     buffer.put((byte) -3);
  13.     System.out.println(Arrays.toString(buffer.array()));
  14. }
复制代码
输出如下所示,我们往内里写入 5 个数据之后,调用 rewind 方法从头开始写入,所以这时候会覆盖前面 3 个数据。
  1. [-1, -2, -3, 3, 4, 0, 0, 0, 0, 0]
复制代码

4.5 reset 和 mark

reset 一样寻常就是配合 mark 来使用,在内里会设置 position 为上一次 mark 的位置,然后从上一次 mark 的位置开始重新操作,但是要注意的是如果 mark < 0,那么就回抛出异常。
  1. public final Buffer reset() {
  2.     // 重新设置 position 为上一次标记的位置
  3.     int m = mark;
  4.     if (m < 0)
  5.         throw new InvalidMarkException();
  6.     position = m;
  7.     return this;
  8. }
  9. public final Buffer mark() {
  10.         // 标记 position 的位置
  11.      mark = position;
  12.      return this;
  13. }
复制代码

在上面的方法中,reset 会让 position 重新回到一开始切换到读模式下标志的 position 位置,也就能实现从头开始重新读取了。

4.6 remaining 和 hasRemaining

remaining 是获取剩下可操作的元素数量,所谓可操作的元素数量,就是当前 position 距离 limit 的位置。
  1. public final int remaining() {
  2.     int rem = limit - position;
  3.     return rem > 0 ? rem : 0;
  4. }
  5. public final boolean hasRemaining() {
  6.     return position < limit;
  7. }
复制代码
下面可以来看下差别模式的剩余可操作元素。

  • 读模式,内里绿色的 1-4 就是读模式下可读元素个数



  • 写模式,内里黄色的 4-9 就是写模式下可读元素个数




4.7 其他的工具方法

Buffer 内里比较核心的方法上面已经先容了,下面是一些工具方法。
4.7.1 isReadOnly - 抽象方法

  1. /**
  2. * Tells whether or not this buffer is read-only.
  3. *
  4. * @return  <tt>true</tt> if, and only if, this buffer is read-only
  5. */
  6. public abstract boolean isReadOnly();
复制代码
这里就是判断创建出来的 Buffer 是否是只读的,也就是说创建出来的 Buffer 不可写。

4.7.2 hasArray - 抽象方法

上面说过了,Buffer 内里的最核心的实现类就是 HeapByteBuffer、DirectByteBuffer、MappedByteBuffer,此中只有 HeapByteBuffer 是 JVM 直接受理的,所以这个方法就是判断 Buffer 有没有一个数组作为数据存储的媒介,也就是判断这个 Buffer 是不是 HeapByteBuffer。
  1. public abstract boolean hasArray();
复制代码

4.7.3 array - 抽象方法

上面的 hasArray 方法就是判断是否有一个数组作为支持,那么这个方法 array 就是获取背后的支持数组。
  1. public abstract Object array();
复制代码

4.7.4 arrayOffset - 抽象方法

这个方法用于返回 Buffer 在其底层数组中的偏移量,其实重要用于获取 Buffer 中第一个元素在底层数组中的详细位置。如果 Buffer 是基于数组实现的,那么返回的就是第一个元素在底层数组中的索引,所以这个方法必要配合 hasArray 来使用,比如下面这个例子
  1. public static void arrayTest(){
  2.     // 创建一个基于数组的 ByteBuffer
  3.     ByteBuffer buffer = ByteBuffer.wrap(new byte[]{10, 20, 30, 40, 50});
  4.     // 确认 Buffer 有底层数组
  5.     if (buffer.hasArray()) {
  6.         // 获取底层数组
  7.         byte[] array = buffer.array();
  8.         // 获取 arrayOffset
  9.         int offset = buffer.arrayOffset();
  10.         // 打印 Buffer 的状态和 arrayOffset
  11.         System.out.println("Buffer position: " + buffer.position());
  12.         System.out.println("Buffer limit: " + buffer.limit());
  13.         System.out.println("Buffer capacity: " + buffer.capacity());
  14.         System.out.println("Array offset: " + offset);
  15.         // 打印底层数组中的数据
  16.         System.out.println("Data in backing array:");
  17.         for (int i = 0; i < array.length; i++) {
  18.             System.out.println("array[" + i + "] = " + array[i]);
  19.         }
  20.         // 打印 Buffer 中的数据(通过 array 和 offset)
  21.         System.out.println("Data in Buffer:");
  22.         for (int i = 0; i < buffer.limit(); i++) {
  23.             System.out.println("Buffer element at " + i + ": " + array[offset + i]);
  24.         }
  25.     } else {
  26.         System.out.println("Buffer does not have an accessible backing array.");
  27.     }
  28. }
复制代码
输出如下:
  1. Buffer position: 0
  2. Buffer limit: 5
  3. Buffer capacity: 5
  4. Array offset: 0
  5. Data in backing array:
  6. array[0] = 10
  7. array[1] = 20
  8. array[2] = 30
  9. array[3] = 40
  10. array[4] = 50
  11. Data in Buffer:
  12. Buffer element at 0: 10
  13. Buffer element at 1: 20
  14. Buffer element at 2: 30
  15. Buffer element at 3: 40
  16. Buffer element at 4: 50
  17. Process finished with exit code 0
复制代码
那如果创建的 Buffer 没有一个数组作为底层的支持,结果又会怎么样呢,比如 DirectByteBuffer。
  1. public static void arrayDirectTest(){
  2.     ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
  3.     int i = byteBuffer.arrayOffset();
  4.     System.out.println(i);
  5. }
复制代码
上面这个例子就会抛出 UnsupportedOperationException 异常,因为 DirectByteBuffer 底层没有一个数组作为支持。

最后这个方法有两种情况会抛出两种异常:

  • ReadOnlyBufferException:如果 Buffer 是基于数组实现的,但它是一个只读的 Buffer(即无法修改此中的数据),那么调用 arrayOffset() 方法会抛出 ReadOnlyBufferException
  • UnsupportedOperationException:就像上面的例子,如果该 Buffer 底层没有一个数组来支持,那么调用这个方法就会抛出 UnsupportedOperationException

4.7.5 isDirect - 抽象方法

这个方法用来判断底层是不是使用直接内存的。
  1. public abstract boolean isDirect();
复制代码

4.7.6 nextGetIndex

这个方法会获取当前 position 指针,同时让 position 指针 + 1。
  1. final int nextGetIndex() {
  2.     int p = position;
  3.     if (p >= limit)
  4.         throw new BufferUnderflowException();
  5.     position = p + 1;
  6.     return p;
  7. }
  8. /**
  9. * 指定增加nb步长
  10. * @param nb
  11. * @return
  12. */
  13. final int nextGetIndex(int nb) {
  14.     int p = position;
  15.     if (limit - p < nb)
  16.         throw new BufferUnderflowException();
  17.     position = p + nb;
  18.     return p;
  19. }
复制代码
那么为什么一个方法是让指针 position + 1,一个是让指针 position + nb 呢?因为 Buffer 有多种范例,如 int、char、byte ... ,如果是 int 范例,那么添加到 ByteBuffer 内里就会占用 4 个字节的位置,所以这时候 nextGetIndex 就必要往后移动 4 个步长。


4.7.7 nextPutIndex

这个 nextPutIndex 就是获取下一个可写入的位置,跟上面的逻辑是一样的。
  1. /**
  2. *
  3. * 获取Buffer下一个可写入的位置
  4. * @return  The current position value, before it is incremented
  5. */
  6. final int nextPutIndex() {
  7.     int p = position;
  8.     if (p >= limit)
  9.         throw new BufferOverflowException();
  10.     position = p + 1;
  11.     return p;
  12. }
  13. /**
  14. * 往Buffer里面写入一个int数据
  15. * @param nb
  16. * @return
  17. */
  18. final int nextPutIndex(int nb) { int p = position;
  19.     if (limit - p < nb)
  20.         throw new BufferOverflowException();
  21.     position = p + nb;
  22.     return p;
  23. }
复制代码

4.7.8 checkIndex 查抄下标是否合法

  1. final int checkIndex(int i) {
  2.     if ((i < 0) || (i >= limit))
  3.         throw new IndexOutOfBoundsException();
  4.     return i;
  5. }
  6. final int checkIndex(int i, int nb) {
  7.     if ((i < 0) || (nb > limit - i))
  8.         throw new IndexOutOfBoundsException();
  9.     return i;
  10. }
复制代码
这里就是查抄下标 i 是不是在可写大概可读的范围内,也就是查抄下标是不是合法的。

4.7.9 truncate 销毁 Buffer

  1. final void truncate() {
  2.    mark = -1;
  3.    position = 0;
  4.    limit = 0;
  5.    capacity = 0;
  6. }
复制代码
这个方法销毁 Buffer 的时候,底层的逻辑就是修改这几个指针,因为 Buffer 是最底层的类,并不会现实存储数据,所以这里只会重置这几个指针,除了在这个方法,下面的 discardMark 也是差不多的,就是重置 mark 值。
  1. final void discardMark() {
  2.      mark = -1;
  3. }
复制代码

4.7.10 checkBounds

这里就是查抄范围的,内里会传入一个偏移量 off,要写入的长度 len,size 就是 buffer 的长度。
  1. static void checkBounds(int off, int len, int size) { // package-private
  2.     if ((off | len | (off + len) | (size - (off + len))) < 0)
  3.         throw new IndexOutOfBoundsException();
  4. }
复制代码

5. 小结

好了,Buffer 的讲解就讲到这里,Buffer 是最底层的一个类,内里涉及到的就是指针的移动,详细对载体(数组)的操作还要到更上层的类如 HeapByteBuffer 内里去看。




如有错误,欢迎指出!!!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表