NIOBuffer(缓冲器)
Java NIO主要解决了Java IO的效率问题,解决此问题的思路之一是利用硬件和操作系统直接支持的缓冲区、虚拟内存、磁盘控制器直接读写等优化IO的手段;思路之二是提供新的编程架构使得单个线程可以控制多个IO,从而节约线程资源,提高IO性能。
Java IO引入了三个主要概念,即缓冲区(Buffer)、通道(Channel)和选择器(Selector),本文主要介绍缓冲区。
1. 缓冲区概念
缓冲区是对Java原生数组的对象封装,它除了包含其数组外,还带有四个描述缓冲区特征的属性以及一组用来操作缓冲区的API。缓冲区的根类是Buffer,其重要的子类包括ByteBuffer、MappedByteBuffer、CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer。从其名称可以看出这些类分别对应了存储不同类型数据的缓冲区。
1.1四个属性
缓冲区由四个属性指明其状态。
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。初始设定后不能更改。
上界(Limit):缓冲区中第一个不能被读或者写的元素位置。或者说,缓冲区内现存元素的上界。
位置(Position):缓冲区内下一个将要被读或写的元素位置。在进行读写缓冲区时,位置会自动更新。
标记(Mark):一个备忘位置。初始时为“未定义”,调用mark时mark=positon,调用reset时position=mark。
这四个属性总是满足如下关系:
mark<=position<=limit<=capacity
一个新分配的10容量的缓冲区如下图所示:
读取和设置四个属性的代码如下:
/**
* 测试Buffer的各种属性
*/
privatestatic void testProperties() {
CharBuffer buffer = CharBuffer.allocate(10);
//buffer的初始状态
showBuffer(buffer);
//存入三个字符后的状态
buffer.put("abc");
showBuffer(buffer);
//flip后的状态
buffer.flip();
showBuffer(buffer);
//读取两个字符后的状态
charc1= buffer.get();
charc2=buffer.get();
showBuffer(buffer);
//clear后的状态
buffer.clear();
showBuffer(buffer);
}
/**
* 显示buffer的position、limit、capacity和buffer中包含的字符,若字符为0,则替换为'.'
*@param buffer
*/
privatestatic void showBuffer(CharBuffer buffer) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < buffer.limit(); i++) {
char c = buffer.get(i);
if (c == 0) {
c = '.';
}
sb.append(c);
}
System.out.printf("position=%d, limit=%d, capacity=%d,content=%s\n",
buffer.position(),buffer.limit(),buffer.capacity(),sb.toString());
}
mark()是为了暂时性的记住一个position的位置,以便在恰当的时候调用reset()方法让position恢复到此位置,其用法如下:
privatestatic void testMark() {
CharBuffer buffer = CharBuffer.allocate(10);
showBuffer(buffer);
//设置mark位置为3
buffer.position(3).mark().position(5);
showBuffer(buffer);
//reset后,position=mark
buffer.reset();
showBuffer(buffer);
}
1.2写入和读取缓冲区中的数据
所有Buffer都有get和put函数,用来写入和读取数据。注意,每put一个元素,position自动加1;而每get一个元素,position也自动加1。能够写入或读取的元素位置,必定位于position和limit之间。有的文章说Buffer有读模式和写模式之分,其实并无此事,只要是position和limit之间的位置,都能够读写,读还是写,存乎程序员之心。
典型的写入代码如下:
privatestatic void testPut() {
CharBuffer buffer = CharBuffer.allocate(10);
//第一种put方法
buffer.put('a').put('b').put('c');
buffer.clear();
//第二种put方法
char[] chars = {'a', 'b', 'c'};
buffer.put(chars);
buffer.clear();
//CharBuffer还可以使用String
buffer.put("abc");
}
典型的读取代码如下:
privatestatic void testGet() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abc");
buffer.flip();
//第一种读取方法
charc1 = buffer.get();
charc2 = buffer.get();
charc3 = buffer.get();
buffer.clear();
//第二种读取方法
buffer.put("abc");
buffer.flip();
char[] chars = newchar[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
其实混合读写也不会报错,例如:
privatestatic void mixPutAndGet() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abc");
buffer.get();
buffer.put("def");
showBuffer(buffer);
//读取此buffer的内容
buffer.flip();
char[] chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
在put的过程中加入了一个get,其效果就是把position向后移动了一位,导致那个位置没有存入字符。
1.3 remaining和hasRemaining
remaining()会返回缓冲区中目前存储的元素个数,在使用参数为数组的get方法中,提前知道缓冲区存储的元素个数是非常有用的。
事实上,由于缓冲区的读或者写模式并不清晰,因此实际上remaining()返回的仅仅是limit – position的值。
而hasRemaining()的含义是查询缓冲区中是否还有元素,这个方法的好处是它是线程安全的。
1.4 Flip翻转
在从缓冲区中读取数据时,get方法会从position的位置开始,依次读取数据,每次读取后position会自动加1,直至position到达limit处为止。因此,在写入数据后,开始读数据前,需要设置position和limit的值,以便get方法能够正确读入前面写入的元素。
这个设置应该是让limit=position,然后position=0,为了方便,Buffer类提供了一个方法flip(),来完成这个设置。其代码如下:
/**
* 测试flip操作,flip就是从写入转为读出前的一个设置buffer属性的操作,其意义是将limit=position,position=0
*/
privatestatic void testFlip() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abc");
buffer.flip();
char[] chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
//以下操作与flip等同
buffer.clear();
buffer.put("abc");
buffer.limit(buffer.position());
buffer.position(0);
chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
1.5compact压缩
压缩compact()方法是为了将读取了一部分的buffer,其剩下的部分整体挪动到buffer的头部(即从0开始的一段位置),便于后续的写入或者读取。其含义为limit=limit-position,position=0,测试代码如下:
privatestatic void testCompact() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
buffer.flip();
//先读取两个字符
buffer.get();
buffer.get();
showBuffer(buffer);
//压缩
buffer.compact();
//继续写入
buffer.put("fghi");
buffer.flip();
showBuffer(buffer);
//从头读取后续的字符
char[] chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
1.6duplicate复制
复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。
privatestatic void testDuplicate() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
CharBuffer buffer1 = buffer.duplicate();
buffer1.clear();
buffer1.put("alex");
showBuffer(buffer);
showBuffer(buffer1);
}
1.7 slice缓冲区切片
缓冲区切片,将一个大缓冲区的一部分切出来,作为一个单独的缓冲区,但是它们公用同一个内部数组。切片从原缓冲区的position位置开始,至limit为止。原缓冲区和切片各自拥有自己的属性,测试代码如下:
privatestatic void testSlice() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcdefghij");
buffer.position(5);
CharBufferslice = buffer.slice();
showBuffer(buffer);
showBuffer(slice);
}
2. 字节缓冲区
为了便于示例,前面的例子都使用了CharBuffer缓冲区,但实际上应用最广,使用频率最高,也是最重要的缓冲区是字节缓冲区ByteBuffer。因为ByteBuffer中直接存储字节,所以在不同的操作系统、硬件平台、文件系统和JDK之间传递数据时不涉及编码、解码和乱码问题,也不涉及Big-Endian和Little-Endian大小端问题,所以它是使用最为便利的一种缓冲区。
2.1视图缓冲区
ByteBuffer中存储的是字节,有时为了方便,可以使用asCharBuffer()等方法将ByteBuffer转换为存储某基本类型的视图,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer和FloatBuffer。
如此转换后,这两个缓冲区共享同一个内部数组,但是对数组内元素的视角不同。以CharBuffer和ByteBuffer为例,ByteBuffer将其视为一个个的字节(1个字节),而CharBuffer则将其视为一个个的字符(2个字节)。若此ByteBuffer的capacity为12,则对应的CharBuffer的capacity为12/2=6。与duplicate创建的复制缓冲区类似,该CharBuffer和ByteBuffer也各自管理自己的缓冲区属性。
还有一点需要注意的是,在创建视图缓冲区的时候ByteBuffer的position属性的取值很重要,视图会以当前position的值为开头,以limit为结尾。例子如下:
private static void testElementView() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//存入四个字节,0x00000042
buffer.put((byte) 0x00).put((byte)0x00).put((byte) 0x00).put((byte) 0x42);
buffer.position(0);
//转换为IntBuffer,并取出一个int(四个字节)
IntBuffer intBuffer =buffer.asIntBuffer();
int i =intBuffer.get();
System.out.println(Integer.toHexString(i));
}
不同元素需要的字节数不同:char为2字节,short为2字节,int为4字节,float为4字节,long为8字节,double也是8字节。
2.2存取数据元素
也可以不通过视图缓冲区,直接向ByteBuffer中存入和取出不同类型的元素,其方法名为putChar()或者getChar()之类。例子如下:
private static void testPutAndGetElement() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//直接存入一个int
buffer.putInt(0x1234abcd);
//以byte分别取出
buffer.position(0);
byte b1 = buffer.get();
byte b2 = buffer.get();
byte b3 = buffer.get();
byte b4 = buffer.get();
System.out.println(Integer.toHexString(b1&0xff));
System.out.println(Integer.toHexString(b2&0xff));
System.out.println(Integer.toHexString(b3&0xff));
System.out.println(Integer.toHexString(b4&0xff));
}
2.3 字节序
终于又要讲到字节序了,这个问题我在上一篇文章中已经详细说过,参见https://zhuanlan.zhihu.com/p/25435644。
简单说来,当某个元素(char、int、double)的长度超过了1个字节时,则由于种种历史原因,它在内存中的存储方式有两种,一种是Big-Endian,一种是Little-Endian。
Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 简单来说,就是我们人类熟悉的存放方式。
Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
Java默认是使用Big-Endian的,因此上面的代码都是以这种方式来存放元素的。但是,其他的一些硬件(CPU)、操作系统或者语言可能是以Little-Endian的方式来存储元素的。因此NIO提供了相应的API来支持缓冲区设置为不同的字节序,其方法很简单,代码如下:
privatestatic void testByteOrder() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//直接存入一个int
buffer.putInt(0x1234abcd);
buffer.position(0);
intbig_endian= buffer.getInt();
System.out.println(Integer.toHexString(big_endian));
buffer.rewind();
intlittle_endian=buffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
System.out.println(Integer.toHexString(little_endian));
}
输出为:
1234abcd
cdab3412
使用order方法可以随时设置buffer的字节序,其参数取值为ByteOrder.LITTLE_ENDIAN以及ByteOrder.BIG_ENDIAN。
2.4直接缓冲区
最后一个需要掌握的概念是直接缓冲区,它是以创建时的开销换取了IO时的高效率。另外一点是,直接缓冲区使用的内存是直接调用了操作系统api分配的,绕过了JVM堆栈。
直接缓冲区通过ByteBuffer.allocateDirect()方法创建,并可以调用isDirect()来查询一个缓冲区是否为直接缓冲区。
一般来说,直接缓冲区是最好的IO选择。
3. 小结
与Stream相比,Buffer引入了更多的概念和复杂性,这一切的努力都是为了实现NIO的经典编程模式,即用一个线程来控制多路IO,从而极大的提高服务器端IO效率。Buffer、Channel和Selector共同实现了NIO的编程模式,其中Buffer也可以被独立的使用,用来完成缓冲区的功能。
- 点赞
- 收藏
- 关注作者
评论(0)