JAVA NIO 之 Buffer

原文:https://segmentfault.com/a/1190000006824155

Java NIO Buffer


当我们需要与 NIO Channel 进行交互时,我们就需要使用到 NIO Buffer,即数据从 Buffer写入到 Channel 中,并且从 Channel 中读取到 Buffer 中。

实际上,NIO Buffer 其实是一块内存区域的封装,并提供了一些操作方法让我们能够方便地进行数据的读写。

Buffer 类型有:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些 Buffer 已经覆盖了能从 IO 中传输的所有的 Java 基本数据类型。

NIO Buffer 的基本使用

使用 NIO Buffer 的步骤如下:

  • 将 Channel 中的数据读取到 Buffer 中,对于 Buffer 本身处于写模式
  • 调用 Buffer.flip() 方法,将 NIO Buffer 转换为读模式.
  • 从 Buffer 中读取数据
  • 调用 Buffer.clear() 或 Buffer.compact() 方法,将 Buffer 转换为写模式

当我们将数据写入到 Buffer 中时,Buffer 会记录我们已经写了多少的数据。当我们需要从 Buffer 中读取数据时,必须调用 Buffer.flip() 将 Buffer 切换为读模式。

一旦读取了所有的 Buffer 数据,那么我们必须清理 Buffer,让其变为重新可写的,清理 Buffer 可以调用 Buffer.clear() 或 Buffer.compact()。

示例:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(12345678);
        intBuffer.put(2);
        intBuffer.flip();
        System.err.println(intBuffer.get());
        System.err.println(intBuffer.get());
    }
}

上面代码中,我们分配两个单位大小的 IntBuffer,因此它可以写入两个 int 值。
我们使用 put 方法将 int 值写入,然后使用 flip 方法将 buffer 转换为读模式,然后连续使用 get 方法从 buffer 中获取这两个 int 值。

每当调用一次 get 方法读取数据时,buffer 的读指针都会向前移动一个单位长度(在这里是一个 int 长度)

Buffer 属性

一个 Buffer 有三个属性:

  • capacity
  • position
  • limit

其中 position 和 limit 的含义与 Buffer 处于读模式或写模式有关,而 capacity 的含义与 Buffer 所处的模式无关。

Capacity

一个内存块会有一个固定的大小,即容量(capacity),我们最多写入 capacity 个单位的数据到 Buffer 中,例如一个 DoubleBuffer,其 Capacity 是 100,那么我们最多可以写入 100 个 double 数据。

Position

当从一个 Buffer 中写入数据时,我们是从 Buffer 的一个确定的位置(position)开始写入的。在最初的状态时,position 的值是 0。每当我们写入了一个单位的数据后,position 就会递增 1。

当我们从 Buffer 中读取数据时,我们也是从某个特定的位置开始读取的。当我们调用了 filp() 方法将 Buffer 从写模式转换到读模式时,position 的值会自动被设置为0。每当我们读取一个单位的数据,position 的值递增 1。

position 表示了读写操作的位置指针。

limit

limit - position 表示此时还可以写入/读取多少单位的数据。
例如在写模式,如果此时 limit 是 10,position 是 2,则表示已经写入了 2 个单位的数据,还可以写入 10 - 2 = 8 个单位的数据。

示例:

public class Test {
    public static void main(String args[]) {
        IntBuffer intBuffer = IntBuffer.allocate(10);
        intBuffer.put(10);
        intBuffer.put(101);
        System.err.println("Write mode: ");
        System.err.println("\tCapacity: " + intBuffer.capacity());
        System.err.println("\tPosition: " + intBuffer.position());
        System.err.println("\tLimit: " + intBuffer.limit());

        intBuffer.flip();
        System.err.println("Read mode: ");
        System.err.println("\tCapacity: " + intBuffer.capacity());
        System.err.println("\tPosition: " + intBuffer.position());
        System.err.println("\tLimit: " + intBuffer.limit());
    }
}

这里我们首先写入两个 int 值,此时 capacity = 10,position = 2,limit = 10;
然后我们调用 flip 转换为读模式, 此时 capacity = 10,position = 0,limit = 2。

分配 Buffer

为了获取一个 Buffer 对象,我们首先需要分配内存空间。每个类型的 Buffer 都有一个 allocate() 方法,我们可以通过这个方法分配 Buffer:

ByteBuffer buf = ByteBuffer.allocate(48);

这里我们分配了 48 * sizeof(Byte) 字节的内存空间。

CharBuffer buf = CharBuffer.allocate(1024);

这里我们分配了大小为 1024 个字符的 Buffer,即这个 Buffer 可以存储 1024 个 Char,其大小为 1024 * 2 个字节。

Direct Buffer 和 Non-Direct Buffer 的区别

Direct Buffer:

  • 所分配的内存不在 JVM 堆上,不受 GC 的管理。(但是 Direct Buffer 的 Java 对象是由 GC 管理的,因此当发生 GC,对象被回收时,Direct Buffer 也会被释放);
  • 因为 Direct Buffer 不在 JVM 堆上分配,因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存,但是 JVM 不好统计到非 JVM 管理的内存)
  • 申请和释放 Direct Buffer 的开销比较大。因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer,然后不断复用此 buffer,在程序结束后才释放此 buffer。
  • 使用 Direct Buffer 时,当进行一些底层的系统 IO 操作时,效率会比较高,因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中。

Non-Direct Buffer:

  • 直接在 JVM 堆上进行内存的分配,本质上是 byte[] 数组的封装。
  • 因为 Non-Direct Buffer 在 JVM 堆中,因此当进行操作系统底层 IO 操作中时,会将此 buffer 的内存复制到中间临时缓冲区中,因此 Non-Direct Buffer 的效率较低。

Buffer 的读写

写入数据到 Buffer

// read into buffer.
int bytesRead = inChannel.read(buf); 
buf.put(127);

从 Buffer 中读取数据

// read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();

重置 position

Buffer.rewind() 方法可以重置 position 的值为0,因此我们可以重新读取/写入 Buffer 了。
如果是读模式,则重置的是读模式的 position,如果是写模式,则重置的是写模式的 position。

示例:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(1);
        intBuffer.put(2);
        System.err.println("position: " + intBuffer.position());

        intBuffer.rewind();
        System.err.println("position: " + intBuffer.position());
        intBuffer.put(1);
        intBuffer.put(2);
        System.err.println("position: " + intBuffer.position());

        intBuffer.flip();
        System.err.println("position: " + intBuffer.position());
        intBuffer.get();
        intBuffer.get();
        System.err.println("position: " + intBuffer.position());

        intBuffer.rewind();
        System.err.println("position: " + intBuffer.position());
    }
}

rewind() 主要针对于读模式,在读模式时,读取到 limit 后,可以调用 rewind() 方法,将读 position 置为 0。

关于 mark() 和 reset()

我们可以通过调用 Buffer.mark() 将当前的 position 的值保存起来,随后可以通过调用 Buffer.reset() 方法将 position 的值恢复回来。

示例:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(1);
        intBuffer.put(2);
        intBuffer.flip();
        System.err.println(intBuffer.get());
        System.err.println("position: " + intBuffer.position());
        intBuffer.mark();
        System.err.println(intBuffer.get());

        System.err.println("position: " + intBuffer.position());
        intBuffer.reset();
        System.err.println("position: " + intBuffer.position());
        System.err.println(intBuffer.get());
    }
}

这里我们写入两个 int 值,然后首先读取了一个值。此时读 position 的值为 1。
接着我们调用 mark() 方法将当前的 position 保存起来(在读模式,因此保存的是读的 position),然后再次读取,此时 position 就是 2 了。
接着使用 reset() 恢复原来的读 position,因此读 position 又为 1 了,可以再次读取数据。

flip, rewind 和 clear 的区别

flip

flip 方法源码

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

Buffer 的读/写模式共用一个 position 和 limit 变量,当从写模式变为读模式时,原先的 写 position 就变成了读模式的 limit。

rewind

rewind 方法源码

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

rewind,即倒带,这个方法仅仅是将 position 置为 0。

clear

clear 方法源码

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

根据源码我们可以知道,clear 将 positin 设置为 0,将 limit 设置为 capacity。

clear 方法使用场景:

  • 在一个已经写满数据的 buffer 中,调用 clear,可以从头读取 buffer 的数据;
  • 为了将一个 buffer 填充满数据,可以调用 clear,然后一直写入,直到达到 limit。

示例:

IntBuffer intBuffer = IntBuffer.allocate(2);
intBuffer.flip();
System.err.println("position: " + intBuffer.position());
System.err.println("limit: " + intBuffer.limit());
System.err.println("capacity: " + intBuffer.capacity());

// 这里不能读, 因为 limit == position == 0, 没有数据.
//System.err.println(intBuffer.get());

intBuffer.clear();
System.err.println("position: " + intBuffer.position());
System.err.println("limit: " + intBuffer.limit());
System.err.println("capacity: " + intBuffer.capacity());

// 这里可以读取数据了, 因为 clear 后, limit == capacity == 2, position == 0,
// 即使我们没有写入任何的数据到 buffer 中.
System.err.println(intBuffer.get()); // 读取到0
System.err.println(intBuffer.get()); // 读取到0

Buffer 的比较

我们可以通过 equals() 或 compareTo() 方法比较两个 Buffer,当且仅当如下条件满足时,两个 Buffer 是相等的:

  • 两个 Buffer 是相同类型的
  • 两个 Buffer 的剩余的数据个数是相同的
  • 两个 Buffer 的剩余的数据都是相同的.

通过上述条件我们可以发现,比较两个 Buffer 时,并不是 Buffer 中的每个元素都进行比较,而是比较 Buffer 中剩余的元素。

你可能感兴趣的:(JAVA NIO 之 Buffer)