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

文章目录

  • [1. 前言](#1. 前言)
  • [2. 概述](#2. 概述)
  • [3. 属性](#3. 属性)
  • [4. 方法](#4. 方法)
    • [4.1 构造器](#4.1 构造器)
    • [4.2 flip()](#4.2 flip())
    • [4.3 clear()](#4.3 clear())
    • [4.4 rewind()](#4.4 rewind())
    • [4.5 reset 和 mark](#4.5 reset 和 mark)
    • [4.6 remaining 和 hasRemaining](#4.6 remaining 和 hasRemaining)
    • [4.7 其他的工具方法](#4.7 其他的工具方法)
      • [4.7.1 isReadOnly - 抽象方法](#4.7.1 isReadOnly - 抽象方法)
      • [4.7.2 hasArray - 抽象方法](#4.7.2 hasArray - 抽象方法)
      • [4.7.3 array - 抽象方法](#4.7.3 array - 抽象方法)
      • [4.7.4 arrayOffset - 抽象方法](#4.7.4 arrayOffset - 抽象方法)
      • [4.7.5 isDirect - 抽象方法](#4.7.5 isDirect - 抽象方法)
      • [4.7.6 nextGetIndex](#4.7.6 nextGetIndex)
      • [4.7.7 nextPutIndex](#4.7.7 nextPutIndex)
      • [4.7.8 checkIndex 检查下标是否合法](#4.7.8 checkIndex 检查下标是否合法)
      • [4.7.9 truncate 销毁 Buffer](#4.7.9 truncate 销毁 Buffer)
      • [4.7.10 checkBounds](#4.7.10 checkBounds)
  • [5. 小结](#5. 小结)

1. 前言

Buffer 是 JDK 1.4 引入的 NIO 包下面的一个核心类,主要是为了提供一种更高效、更灵活的方式来进行 I/O 操作。

对于传统的 IO ,往往会涉及到多次内存的复制,比如从内核态复制到用户态,再赋值到应用程序缓冲区,Buffer 就提供了一种直接映射内存区域的可能,减少这些数据的复制,提高查询的效率。

除此外,Buffer 在 NIO 中用来存储数据的容器,Channel 通过 Buffer 进行读写操作,从而实现高效的 NIO,也就是非阻塞 IO。

关于 Buffer 就简单介绍这么多,其实没有 Buffer 之前,传统的 IO 操作通过输入输出流来处理,一次只能处理一个字节,性能就不用多说了,是比较低的,有了 Buffer 之后,一次就能读取一批的数据到 Buffer 中来处理,性能从而能大大提高,比如说下面我们要读取一个文件的时候,可以使用 FileChannel 配合 Buffer 来进行读取

java 复制代码
public class FileReaderTest {
    public static void main(String[] args) {
        String filePath = "example.txt";

        try (RandomAccessFile accessFile = new RandomAccessFile(filePath, "r");
             FileChannel fileChannel = accessFile.getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 读取数据到 buffer 缓冲区中
            while (fileChannel.read(buffer) > 0) {
                // 切换读模式
                buffer.flip();
                while (buffer.hasRemaining()) {
                    // 获取剩余字节
                    System.out.print((char) buffer.get());
                }
                // 清空缓冲区,准备下一次读取
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

输出如下:

2. 概述

上面就介绍了下 Buffer 的作用和为什么要引入 Buffer,那么既然 Buffer 可以一次性处理那么多的数据,那这些数据怎么存储的呢?既然 Buffer 可写可读,那么切换模式的时候是怎么确保上一次没有读完的数据不会被覆盖的?...

要解释上面的问题,就要去看 Buffer 的源码,那再看具体的源码逻辑之前,我们先看里面的属性。

3. 属性

private int mark = -1

mark 是 Buffer 中的一个标记位,用来标记原来的 position 的位置。

  • 当 Buffer 中需要去处理其他位置的数据的时候,可以使用 mark 先标记一下原来的位置,等处理完再回到原来的 position 继续处理
  • 如果调用者想要多此读取同一部分的数据,可以使用 mark 来标记原来的 position,读完之后再设置 position = mark,就可以重复读取了

下面来看下 mark 的用法,我们用 mark 来实现对 Buffer 的一段数据重复读:

java 复制代码
public class MarkTest {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);

        buffer.put(1);
        buffer.put(2);
        buffer.put(3);
        buffer.put(4);

        // 切换读模式
        buffer.flip();
        // 做下标记
        buffer.mark();

        System.out.println(buffer.get());  // 1
        System.out.println(buffer.get());  // 2

        // 回到标记的位置,就是下标 0 的位置
        buffer.reset();
        System.out.println(buffer.get()); // 1
        System.out.println(buffer.get()); // 2
    }
}

可以看到,上面再 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 的构造器需要指定 markposlimitcap 四个属性。

java 复制代码
/**
 * Creates a new buffer with the given mark, position, limit, and capacity, after checking invariants.
 * @param mark
 * @param pos
 * @param lim
 * @param cap
 */
Buffer(int mark, int pos, int lim, int cap) {       // package-private
    if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
    // 1.设置capacity
    this.capacity = cap;
    // 2.设置limit
    limit(lim);
    // 3.设置position
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: ("
                                               + mark + " > " + pos + ")");
        // 4.设置mark
        this.mark = mark;
    }
}

上面构造器的源码就是在设置这几个属性,那么来看下 limit 的逻辑。

java 复制代码
/**
 * Sets this buffer's limit.  If the position is larger than the new limit
 * then it is set to the new limit.  If the mark is defined and larger than
 * the new limit then it is discarded.
 * 设置limit
 *
 * @param  newLimit
 *         The new limit value; must be non-negative
 *         and no larger than this buffer's capacity
 *
 * @return  This buffer
 *
 * @throws  IllegalArgumentException
 *          If the preconditions on <tt>newLimit</tt> do not hold
 */
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    // 设置limit
    limit = newLimit;
    // 如果position比新的limit要大,就需要更新position到最新的newLimit
    if (position > newLimit) position = newLimit;
    // 如果mark比新的limit要大,就重置mark
    if (mark > newLimit) mark = -1;
    return this;
}

设置 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 的逻辑。

java 复制代码
public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
        throw createPositionException(newPosition);
    // 如果mark比新的newPosition要大,就重置下
    if (mark > newPosition) mark = -1;
    position = newPosition;
    return this;
}

设置 position 的时候,需要判断不能比 limit 大,同时要设置下 mark,如果 mark 比新的 newPosition 要大,就说明 position 指针往左移动了,这时候 mark 标记的就是无效数据了,所以设置为 -1,看下面的图。

上面图中 limit 标记的就是无用的位置了。因为 newPosition 指向下标 3,在写模式下会从下标 3 继续写入,所以这时候 limit 位置的会被覆盖,所以说 limit 就是一个无效数据了,下面还是看一个例子吧。

java 复制代码
public static void bufferTest(){
     ByteBuffer buffer = ByteBuffer.allocate(10);

     buffer.put((byte) 1);
     buffer.put((byte) 2);
     buffer.put((byte) 3);
     buffer.put((byte) 4);
     buffer.put((byte) 5);
     // 做下标记
     buffer.mark();

     buffer.position(3);
     buffer.put((byte) 6); // 1 2 3 6 5

     // 回到标记的位置
     buffer.reset(); // 抛出异常
 }

最终会抛出异常:

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

4.2 flip()

上面说过,Buffer 分为两种模式:读模式和写模式,读模式就是专门读取的,看源码:

java 复制代码
public final Buffer flip() {
    // limit 设置成写模式下的 position,读模式的范围就是 [0, position)
    limit = position;
    // 读模式下 position 设置为 0,这样就可以从头开始读取 Buffer 中的数据了
    position = 0;
    // mark 重置为 -1
    mark = -1;
    return this;
}

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

4.3 clear()

有读模式,就有写模式,写模式顾名思义就是继续往里面写入数据,但是我们这里只是最底层的抽象逻辑,所以和上面的 flip 一样,只是调整几个参数。

java 复制代码
/**
 * 切换写模式,假设现在数组指针如下:                    capacity
 * mark                position                     limit
 *  -1   0    1   2       3      4    5    6    7     8
 * 切换之后:
 *                                                      capacity
 * mark  position                                        limit
 *  -1      0       1    2     3      4    5    6    7     8
 *
 * 但是上面的转换有一个问题,如果转换之前我们并没有读完数据,也就是说 [position, limit) 里面的数据还没有读取
 * 这时候切换 position 为 0,后续写入不久覆盖了吗
 * 所以针对这种情况,我们就需要把 [position, limit) 的数据拷贝到前面,然后再移动数据
 *                                                       capacity
 * mark           不可覆盖             position    可覆盖    limit
 *  -1    0     1    2     3      4      5        6    7     8
 *
 * 由于 Buffer 是顶层的接口,所以上面的移动就交给了子类来实现,比如 HeapByteBuffer 的 compact
 *
 * @return  This buffer
 */
public final Buffer clear() {
    // 设置为 0,从 0 开始进行写入数据
    position = 0;
    // 重新设置 limit
    limit = capacity;
    // 重新设置 mark
    mark = -1;
    return this;
}

4.4 rewind()

rewind() 方法是 Java NIO Buffer 类中的一个重要方法,它的主要作用是重置 position,同时丢弃 mark。

在读取或者写入操作之前,可以调用这个方法回到初始状态,重新处理数据。

  1. 重新读取数据

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

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

下面就是这个方法的源码,也就是重新设置 position 和 mark 标记。

java 复制代码
public final Buffer rewind() {
    // 重置 position 和 mark
    position = 0;
    mark = -1;
    return this;
}

下面有一个例子:

java 复制代码
public static void rewindTest(){
    ByteBuffer buffer = ByteBuffer.allocate(10);
    buffer.put((byte) 0);
    buffer.put((byte) 1);
    buffer.put((byte) 2);
    buffer.put((byte) 3);
    buffer.put((byte) 4);

    // 从头开始写入数据
    buffer.rewind();
    buffer.put((byte) -1);
    buffer.put((byte) -2);
    buffer.put((byte) -3);

    System.out.println(Arrays.toString(buffer.array()));
}

输出如下所示,我们往里面写入 5 个数据之后,调用 rewind 方法从头开始写入,所以这时候会覆盖前面 3 个数据。

java 复制代码
[-1, -2, -3, 3, 4, 0, 0, 0, 0, 0]

4.5 reset 和 mark

reset 一般就是配合 mark 来使用,在里面会设置 position 为上一次 mark 的位置,然后从上一次 mark 的位置开始重新操作,但是要注意的是如果 mark < 0,那么就回抛出异常。

java 复制代码
public final Buffer reset() {
    // 重新设置 position 为上一次标记的位置
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

public final Buffer mark() {
	// 标记 position 的位置
     mark = position;
     return this;
 }

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

4.6 remaining 和 hasRemaining

remaining 是获取剩下可操作的元素数量,所谓可操作的元素数量,就是当前 position 距离 limit 的位置。

java 复制代码
public final int remaining() {
    int rem = limit - position;
    return rem > 0 ? rem : 0;
}

public final boolean hasRemaining() {
    return position < limit;
}

下面可以来看下不同模式的剩余可操作元素。

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

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

4.7 其他的工具方法

Buffer 里面比较核心的方法上面已经介绍了,下面是一些工具方法。

4.7.1 isReadOnly - 抽象方法

java 复制代码
/**
 * Tells whether or not this buffer is read-only.
 *
 * @return  <tt>true</tt> if, and only if, this buffer is read-only
 */
public abstract boolean isReadOnly();

这里就是判断创建出来的 Buffer 是否是只读的,也就是说创建出来的 Buffer 不可写。

4.7.2 hasArray - 抽象方法

上面说过了,Buffer 里面的最核心的实现类就是 HeapByteBufferDirectByteBufferMappedByteBuffer,其中只有 HeapByteBuffer 是 JVM 直接管理的,所以这个方法就是判断 Buffer 有没有一个数组作为数据存储的媒介,也就是判断这个 Buffer 是不是 HeapByteBuffer。

java 复制代码
public abstract boolean hasArray();

4.7.3 array - 抽象方法

上面的 hasArray 方法就是判断是否有一个数组作为支撑,那么这个方法 array 就是获取背后的支撑数组。

java 复制代码
public abstract Object array();

4.7.4 arrayOffset - 抽象方法

这个方法用于返回 Buffer 在其底层数组中的偏移量,其实主要用于获取 Buffer 中第一个元素在底层数组中的具体位置。如果 Buffer 是基于数组实现的,那么返回的就是第一个元素在底层数组中的索引,所以这个方法需要配合 hasArray 来使用,比如下面这个例子

java 复制代码
public static void arrayTest(){
    // 创建一个基于数组的 ByteBuffer
    ByteBuffer buffer = ByteBuffer.wrap(new byte[]{10, 20, 30, 40, 50});

    // 确认 Buffer 有底层数组
    if (buffer.hasArray()) {
        // 获取底层数组
        byte[] array = buffer.array();

        // 获取 arrayOffset
        int offset = buffer.arrayOffset();

        // 打印 Buffer 的状态和 arrayOffset
        System.out.println("Buffer position: " + buffer.position());
        System.out.println("Buffer limit: " + buffer.limit());
        System.out.println("Buffer capacity: " + buffer.capacity());
        System.out.println("Array offset: " + offset);

        // 打印底层数组中的数据
        System.out.println("Data in backing array:");
        for (int i = 0; i < array.length; i++) {
            System.out.println("array[" + i + "] = " + array[i]);
        }

        // 打印 Buffer 中的数据(通过 array 和 offset)
        System.out.println("Data in Buffer:");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.println("Buffer element at " + i + ": " + array[offset + i]);
        }
    } else {
        System.out.println("Buffer does not have an accessible backing array.");
    }
}

输出如下:

java 复制代码
Buffer position: 0
Buffer limit: 5
Buffer capacity: 5
Array offset: 0
Data in backing array:
array[0] = 10
array[1] = 20
array[2] = 30
array[3] = 40
array[4] = 50
Data in Buffer:
Buffer element at 0: 10
Buffer element at 1: 20
Buffer element at 2: 30
Buffer element at 3: 40
Buffer element at 4: 50

Process finished with exit code 0

那如果创建的 Buffer 没有一个数组作为底层的支撑,结果又会怎么样呢,比如 DirectByteBuffer。

java 复制代码
public static void arrayDirectTest(){
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
    int i = byteBuffer.arrayOffset();
    System.out.println(i);
}

上面这个例子就会抛出 UnsupportedOperationException 异常,因为 DirectByteBuffer 底层没有一个数组作为支撑。

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

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

4.7.5 isDirect - 抽象方法

这个方法用来判断底层是不是使用直接内存的。

java 复制代码
public abstract boolean isDirect();

4.7.6 nextGetIndex

这个方法会获取当前 position 指针,同时让 position 指针 + 1。

java 复制代码
final int nextGetIndex() {
    int p = position;
    if (p >= limit)
        throw new BufferUnderflowException();
    position = p + 1;
    return p;
}

/**
 * 指定增加nb步长
 * @param nb
 * @return
 */
final int nextGetIndex(int nb) {
    int p = position;
    if (limit - p < nb)
        throw new BufferUnderflowException();
    position = p + nb;
    return p;
}

那么为什么一个方法是让指针 position + 1,一个是让指针 position + nb 呢?因为 Buffer 有多种类型,如 int、char、byte ... ,如果是 int 类型,那么添加到 ByteBuffer 里面就会占用 4 个字节的位置,所以这时候 nextGetIndex 就需要往后移动 4 个步长。

4.7.7 nextPutIndex

这个 nextPutIndex 就是获取下一个可写入的位置,跟上面的逻辑是一样的。

java 复制代码
/**
 *
 * 获取Buffer下一个可写入的位置
 * @return  The current position value, before it is incremented
 */
final int nextPutIndex() {
    int p = position;
    if (p >= limit)
        throw new BufferOverflowException();
    position = p + 1;
    return p;
}

/**
 * 往Buffer里面写入一个int数据
 * @param nb
 * @return
 */
final int nextPutIndex(int nb) { int p = position;
    if (limit - p < nb)
        throw new BufferOverflowException();
    position = p + nb;
    return p;
}

4.7.8 checkIndex 检查下标是否合法

java 复制代码
final int checkIndex(int i) {
    if ((i < 0) || (i >= limit))
        throw new IndexOutOfBoundsException();
    return i;
}

final int checkIndex(int i, int nb) {
    if ((i < 0) || (nb > limit - i))
        throw new IndexOutOfBoundsException();
    return i;
}

这里就是检查下标 i 是不是在可写或者可读的范围内,也就是检查下标是不是合法的。

4.7.9 truncate 销毁 Buffer

java 复制代码
final void truncate() {
   mark = -1;
   position = 0;
   limit = 0;
   capacity = 0;
}

这个方法销毁 Buffer 的时候,底层的逻辑就是修改这几个指针,因为 Buffer 是最底层的类,并不会实际存储数据,所以这里只会重置这几个指针,除了在这个方法,下面的 discardMark 也是差不多的,就是重置 mark 值。

java 复制代码
final void discardMark() {
     mark = -1;
 }

4.7.10 checkBounds

这里就是检查范围的,里面会传入一个偏移量 off,要写入的长度 len,size 就是 buffer 的长度。

java 复制代码
static void checkBounds(int off, int len, int size) { // package-private
    if ((off | len | (off + len) | (size - (off + len))) < 0)
        throw new IndexOutOfBoundsException();
}

5. 小结

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

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

相关推荐
star-yp1 分钟前
vibe coding 博客管理系统
java·spring boot·spring·ai·ai编程
小江的记录本3 分钟前
【JEECG Boot】JEECG Boot 系统性知识体系全方位结构化总结
java·前端·spring boot·后端·python·spring·spring cloud
Mr.wangh3 分钟前
Spring原理(Bean的生命周期)
java·前端·spring
派大星酷7 分钟前
Java 多线程创建方式
java·开发语言·多线程
棉花骑士9 小时前
【AI Agent】面向 Java 工程师的Claude Code Harness 学习指南
java·开发语言
爱敲代码的小鱼9 小时前
springboot(2)从基础到项目创建:
java·spring boot·spring
迈巴赫车主10 小时前
蓝桥杯19724食堂
java·数据结构·算法·职场和发展·蓝桥杯
i220818 Faiz Ul11 小时前
动漫商城|基于springboot + vue动漫商城系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·动漫商城系统
海兰11 小时前
【实战】MCP 服务在 Nacos 中注册状态分析与优化
android·java·github·银行系统·银行ai
Makoto_Kimur12 小时前
Java 打印模板大全
java·开发语言·排序算法