JDK源码阅读:ByteBuffer

Buffer是Java NIO中对于缓冲区的封装。在Java BIO中,全部的读写API,都是直接使用byte数组做为缓冲区的,简单直接。可是在Java NIO中,缓冲区这一律念变得复杂,多是对应Java堆中的一块内存,也多是对应本地内存中的一块内存。而byte数组只能用来指定Java堆中的一块内存,因此Java NIO中设计了一个新的缓冲区抽象,涵盖了不一样类型缓冲区,这个抽象就是Buffer。数组

Buffer

Buffer是Java NIO中对于缓冲区的抽象。是一个用于存储特定基本数据类型的容器。Buffer是特定基本数据类型的线性有限序列。安全

Java有8中基本类型:byte,short,int,long,float,double,char,boolean,除了boolean类型外,其余的类型都有对应的Buffer具体实现:函数

Buffer抽象类定义了全部类型的Buffer都有的属性和操做,属性以下:this

  • capacity:缓冲区的容量,在缓冲区创建后就不能改变
  • limit:表示第一个不能读写的元素位置,limit不会大于capacity
  • position:表示下一个要读写的元素位置,position不会大于limit
  • mark:用于暂存一个元素位置,和书签同样,用于后续操做

全部的Buffer操做都围绕这些属性进行。这些属性知足一个不变式:0<=mark<=position<=limit<=capacityspa

新建的Buffer这些属性的取值为:操作系统

  • position=0
  • limit=capacity=用户设置的容量
  • mark=-1

直接看定义比较抽象,能够看一下示意图,下图是一个容量为10的Buffer:线程

ByteBuffer的具体实现

全部Buffer实现中,最重要的实现是ByteBuffer,由于操做系统中全部的IO操做都是对字节的操做。当咱们须要从字节缓冲区中读取别的数据类型才须要使用其余具体类型的Buffer实现。设计

ByteBuffer也是一个抽象类,具体的实现有HeapByteBuffer和DirectByteBuffer。分别对应Java堆缓冲区与堆外内存缓冲区。Java堆缓冲区本质上就是byte数组,因此实现会比较简单。而堆外内存涉及到JNI代码实现,较为复杂,本次咱们以HeapByteBuffer为例来分析Buffer的相关操做,后续专门分析DirectByteBuffer。code

ByteBuffer的类图以下:ip

读写Buffer

Buffer做为缓冲区,最主要的做用是用于传递数据。Buffer提供了一系列的读取与写入操做。由于不一样类型的Buffer读写的类型不一样,因此具体的方法定义是定义在Buffer实现类中的。与读写相关的API以下:

1

2

3

4

5

6

7

8

9

byte get()

byte get(int index)

ByteBuffer get(byte[] dst, int offset, int length)

ByteBuffer get(byte[] dst)

 

ByteBuffer put(byte b)

ByteBuffer put(int index, byte b)

ByteBuffer put(ByteBuffer src)

ByteBuffer put(byte[] src, int offset, int length)

Buffer的读写操做能够按照两种维度分类:

  • 单个/批量:
    • 单个:一次读写一个字节
    • 批量:一次读写多个字节
  • 相对/绝对:
    • 相对:从Buffer维护的position位置开始读写,读写时position会随之变化
    • 绝对:直接指定读写的位置。指定index的API就是绝对API

接着咱们来看看这些函数在HeapByteBuffer中是如何实现的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

final byte[] hb;    // 做为缓冲区的byte数组             

final int offset;   // 指定缓冲区的起始位置

 

public byte get() {

    // get操做就是直接从数组中获取数据

    return hb[ix(nextGetIndex())];

}

 

public byte get(int i) {

    // 从指定位置获取数据,是绝对操做,只需检查下标是否合法

    return hb[ix(checkIndex(i))];

}

 

// 获取下一个要读取的元素的下标

// position的定义就是下一个要读写的元素位置,

// 因此这里是返回position的当前值,而后再对position进行加一操做

final int nextGetIndex() {                          // package-private

    if (position >= limit)

        throw new BufferUnderflowException();

    return position++;

}

 

// 由于支持偏移量,因此算出来的下标还须要加上偏移量

protected int ix(int i) {

    return i + offset;

}

单字节put与get逻辑同样。看一下批量get是如何实现的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public ByteBuffer get(byte[] dst) {

    return get(dst, 0, dst.length);

}

 

public ByteBuffer get(byte[] dst, int offset, int length) {

    // 检查参数是否越界

    checkBounds(offset, length, dst.length);

    // 检查要获取的长度是否大于Buffer中剩余的数据长度

    if (length > remaining())

        throw new BufferUnderflowException();

    // 调用System.arraycopy进行数组内容拷贝

    System.arraycopy(hb, ix(position()), dst, offset, length);

    // 更新position

    position(position() + length);

    return this;

}

能够看出,HeapByteBuffer是封装了对byte数组的简单操做。对缓冲区的写入和读取本质上是对数组的写入和读取。使用HeapByteBuffer的好处是咱们不用作各类参数校验,也不须要另外维护数组当前读写位置的变量了。

同时咱们能够看到,Buffer中对于position的操做没有使用锁进行保护,因此Buffer不是线程安全的。

Buffer的模式

虽然JDK的Java Doc并无提到Buffer有模式,可是Buffer提供了flip等操做用于切换Buffer的工做模式。在正确使用Buffer时,必定要注意Buffer的当前工做模式。不然会致使数据读写不符合你的预期。

Buffer有两种工做模式,一种是接收数据模式,一种是输出数据模式。

新建的Buffer处于接收数据的模式,能够向Buffer放入数据,放入一个对应基本类型的数据后,position加一,若是position已经等于limit了还进行put操做,则会抛出BufferOverflowException异常。

这种模式的Buffer能够用于Channel的read操做缓冲区,或者是用于相对put操做。

好比向一个接受数据模式的Buffer put5个byte后的示例图:

由于Buffer的设计是读写的位置变量都使用position这个变量,因此若是要从Buffer中读取数据,要切换Buffer到输出数据模式。Buffer提供了flip方法用于这种切换。

1

2

3

4

5

6

public final Buffer flip() {

    limit = position;

    position = 0;

    mark = -1;

    return this;

}

切换后的效果图:

而后就能够从Buffer中读取数据了。每次读取一个元素,position就会加一,若是position已经等于limit还进行读取,会抛出BufferUnderflowException异常。

能够看出Buffer自己没有一个用于存储模式的变量,模式的切换只是position和limit的变换而已。

flip方法只会把Buffer从接收模式切换到输出模式,若是要从输出模式切换到接收模式,能够使用compact或者clear方法,若是数据已经读取完毕或者数据不要了,使用clear方法,若是已读的数据须要保留,同时须要切换到接收数据模式,使用compat方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// 压缩Buffer,去掉已经被读取的数据

// 压缩后的Buffer处于接收数据模式

public ByteBuffer compact() {

    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());

    position(remaining());

    limit(capacity());

    discardMark();

    return this;

}

 

// 清空Buffer,去掉全部数据(没有作清理工做,是指修改位置变量)

// 清空后的Buffer处于接收数据模式

public final Buffer clear() {

    position = 0;

    limit = capacity;

    mark = -1;

    return this;

}

总结

  • Buffer是Java NIO对缓冲区的抽象
  • 除了boolean类型,其余的基本类型都有对应的Buffer实现
  • 最经常使用的Buffer实现是ByteBuffer,具体的实现有HeapByteBuffer和DirectByteBuffer,分别对应Java堆缓冲区与对外内存缓冲区
  • HeapByteBuffer是对byte数组的封装,方便使用
  • Buffer不是线程安全的
  • Buffer有两种模式一种是接收数据模式,一种是输出数据模式。新建的Buffer处于接收数据模式,使用flip方法能够切换Buffer到输出数据模式。使用compact或者clear方法能够切换到接收数据模式。

参考资料