一、结论
ByteBuffer 是Java NIO体系中的基础类,所有与Channel进行数据交互操作的都是以ByteBuffer作为数据的载体(即缓冲区)。ByteBuffer的底层是byte数组,通过四个重要的成员变量(mark、limit、position、capacity)来实现对缓冲区的读写数据以及复用缓冲区等操作。ByteBuffer 申请缓冲区内存(数组)的方式有两种,即堆内存与堆外内存,其中堆外内存有着较强的性能,但需要小心处理,堆内存则可以放心的交给JVM管理。此外还需要注意一点的是ByteBuffer是非线程安全的。
二、API研究
学习新知识总归要回到其本质上,首先思考以下问题:
如果使用一个数组作为缓冲区,想要复用这个缓冲区需要作什么?
在这里我们以阻塞IO读写文件来说明(代码如下所示)
我们新建了一个数组作为缓冲区,读文件的时候不断的往该缓冲区写入数据,并记录实际读入的字节数,并将实际读入的字节数写入byteOutputStream。
可以看出要想使用一个数组作为缓冲区首先我们至少需要以下数据
1.缓冲区的大小(buffer.length)
2.缓冲区内可用的字节数(即readLength,因为不可能每次读入数据都填满整个缓冲区)
此外,由于InputStream可以将数据读到缓冲区的指定分段,因此缓冲区内可用的字节数实际上是由一个数组下标值(默认为0)加上实际读入的字节数长度组成的。
1 public static void main() throws IOException { 2 byte[] buffer = new byte[1024]; 3 ByteOutputStream byteOutputStream = new ByteOutputStream(); 4 5 File file = new File("D:/tmp/test"); 6 FileInputStream inputStream = new FileInputStream(file); 7 int readLength = 0; 8 while ((readLength = inputStream.read(buffer)) != 0){ 9 // do something 10 byteOutputStream.write(buffer,0,readLength); 11 } 12 inputStream.close(); 13 System.out.println(new String(byteOutputStream.getBytes())); 14 }
因此,ByteBuffer 作为可以复用的缓冲区,其底层也是使用数组作为缓冲区,其核心主要有以下四个成员变量
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
mark 标记
position 位置,当前数组指针所在的位置,即下一个读出\写入数组元素的指针所在位置
limit 缓冲区的限制,第一个不应该向此缓冲区写入\读出数据的位置(对应readLength)(默认等于capacity)
capacity 缓冲区的实际大小(对应buffer.length)
因此ByteBuffer 的API主要是围绕围绕这四个成员变量开展的。
1.以remaining举例说明(该方法用于获取剩余元素的大小)
public final int remaining() { return limit - position; }
(图片来自《NIO与Socket编程指南》)
如上图所示此时 capacity = 8,limit = 6,position = 2 ,以读数据为例则说明此时还有4个元素可供读取。
2.ByteBuffer 复用的实现
参考阻塞读写文件例子可以确定要复用ByteBuffer必然要重置相关的变量,重置不同的变量有着不同的效果。
比如,在向ByteBuffer中写入数据之后,要再读出数据时必然需要知道实际写入数据的长度(limit并不会随着写入数据而改变,limit代表了缓冲区的限制),并且将数组指针移动到0的位置。
filp方法就是干这活的,其实现如下:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this;
}
要还原缓冲区的状态,直接调用clear即可,但该方法并不会清除缓冲区中的数据
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
如果在读取数据的过程(此时postion已经改变)想要再次从头读取数据只需重置postion即可,使用rewind方法即可,该方法会重置mark为-1。
public final Buffer rewind() { position = 0; mark = -1; return this; }
3.Mark 一下
从设计上来说ByteBuffer并不是一个支持随机访问(RandomAccess)的缓冲区,写入或读出数据的时候数组的指针只能向前移动,但在某些场景下我们可能需要对某个数据段的内容进行重复读取,此时只需要对指定的位置执行标记操作以便需要的时候将指针移动到标记的位置。
mark方法如下所示,只是暂存position,并不会改变指针的位置。
public final Buffer mark() { mark = position; return this; }
因此如果想要回到该位置就需要执行reset方法。
public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this;
}
由于mark的默认值为-1,因此如果未执行过mark方法会抛出异常。