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 中剩余的元素。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容