java堆缓冲区,Java NIO之Buffer(缓冲区)

Java NIO主要解决了Java IO的效率问题,解决此问题的思路之一是利用硬件和操作系统直接支持的缓冲区、虚拟内存、磁盘控制器直接读写等优化IO的手段;思路之二是提供新的编程架构使得单个线程可以控制多个IO,从而节约线程资源,提高IO性能。

Java IO引入了三个主要概念,即缓冲区(Buffer)、通道(Channel)和选择器(Selector),本文主要介绍缓冲区。

缓冲区概念

缓冲区是对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的ByteBuffer对象如下图所示:

9291d662f63f

image.png

在初始化的时候,position设置为0,limit和 capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个个将会随着使用而变化。三个属性值分别如图所示:

9291d662f63f

image.png

现在我们可以从通道中读取一些数据到缓冲区中,注意从通道读取数据,相当于往缓冲区中写入数据。如果读取4个字节的数据,则此时position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示:

9291d662f63f

image.png

下一步把读取的数据写入到输出通道中,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法,该方法将会完成两件事情:

把limit设置为当前的position值

把position设置为0

由于position被设置为0,所以可以保证在下一步输出时读取到的是缓冲区中的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入到缓冲区中的数据,如下图所示:

9291d662f63f

image.png

现在调用get()方法从缓冲区中读取数据写入到输出通道,这会导致position的增加而limit保持不变,但position不会超过limit的值,所以在读取我们之前写入到缓冲区中的4个自己之后,position和limit的值都为4,如下图所示:

9291d662f63f

image.png

在从缓冲区中读取数据完毕后,limit的值仍然保持在我们调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值,如下图所示:

9291d662f63f

image.png

下面这个例子可以展示buffer的读写:

public class NioTest1 {

public static void main(String[] args) {

//通过nio生成随机数,然后在打印出来

IntBuffer buffer = IntBuffer.allocate(10);

System.out.println("capacity:"+buffer.capacity());

for (int i = 0;i < 5;i++){

int randomNumber = new SecureRandom().nextInt(20);

//这里相当于把数据写到buffer中

buffer.put(randomNumber);

}

System.out.println("before flip limit:"+buffer.capacity());

//上面是写,下面为读,通过flip()方法进行读写的切换

buffer.flip();

System.out.println("after flip limit:"+buffer.capacity());

System.out.println("enter while loop");

while(buffer.hasRemaining()){

System.out.println("position:" + buffer.position());

System.out.println("limit:" + buffer.limit());

System.out.println("capacity:" + buffer.capacity());

//这里相当于从buffer中读出数据

System.out.println(buffer.get());

}

}

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

*/

private static 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,测试代码如下:

private static 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复制

复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。

private static 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为止。原缓冲区和切片各自拥有自己的属性,测试代码如下:

/**

* slice Buffer 和原有Buffer共享相同的底层数组

*/

public class NioTest6 {

public static void main(String[] args) {

ByteBuffer buffer = ByteBuffer.allocate(10);

for (int i=0;i

buffer.put((byte)i);

}

buffer.position(2);

buffer.limit(6);

ByteBuffer sliceBuffer = buffer.slice();

for (int i=0;i < sliceBuffer.capacity();i++){

byte b = sliceBuffer.get(i);

b *= 2;

sliceBuffer.put(i,b);

}

buffer.position(0);

buffer.limit(buffer.capacity());

while(buffer.hasRemaining()){

System.out.println(buffer.get());

}

}

}

1.8只读Buffer

我们可以随时将一个普通Buffer调用asReadOnlyBuffer方法返回一个只读Buffer,但不能将一个只读Buffer转换成读写Buffer

public class NioTest7 {

public static void main(String[] args) {

ByteBuffer buffer = ByteBuffer.allocate(10);

System.out.println(buffer.getClass());

for (int i=0;i

buffer.put((byte)i);

}

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

System.out.println(readOnlyBuffer.getClass());

//只读buffer,不可以写

// readOnlyBuffer.position(0);

// readOnlyBuffer.put((byte)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 字节序

简单说来,当某个元素(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直接缓冲区 DirectByteBuffer

最后一个需要掌握的概念是直接缓冲区,它是以创建时的开销换取了IO时的高效率。另外一点是,直接缓冲区使用的内存是直接调用了操作系统api分配的,绕过了JVM堆栈。

直接缓冲区通过ByteBuffer.allocateDirect()方法创建,并可以调用isDirect()来查询一个缓冲区是否为直接缓冲区。

一般来说,直接缓冲区是最好的IO选择。

public class NioTest8 {

public static void main(String[] args) throws IOException {

FileInputStream inputStream = new FileInputStream("input2.txt");

FileOutputStream outputStream = new FileOutputStream("output2.txt");

FileChannel inputChannel = inputStream.getChannel();

FileChannel outputChannel = outputStream.getChannel();

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

while(true){

//每一次读取之前都将buffer状态初始化

buffer.clear();

int read = inputChannel.read(buffer);

System.out.println("read:" + read);

if(-1 == read){

break;

}

buffer.flip();

outputChannel.write(buffer);

}

inputChannel.close();

outputChannel.close();

}

}

2.5、MappedByteBuffer 内存映射文件

文件的内容直接映射到内存里面,在内存中任何的信息修改,最终都会被写入到磁盘文件中,即MappedByteBuffer是一种允许java程序直接从内存访问的特殊的文件,可以将整个文件或整个文件的一部分映射到内存中,由操作系统负责将页面请求的内存数据修改写入到文件中。应用程序只需要处理内存的数据,这样可以实现迅速的IO操作,用于内存映射文件的这个内存是堆外内存

public class NioTest9 {

public static void main(String[] args) throws IOException {

RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");

FileChannel fileChannel = randomAccessFile.getChannel();

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 6);

mappedByteBuffer.put(0, (byte) 'a');

mappedByteBuffer.put(4, (byte) 'b');

randomAccessFile.close();

}

}

小结

与Stream相比,Buffer引入了更多的概念和复杂性,这一切的努力都是为了实现NIO的经典编程模式,即用一个线程来控制多路IO,从而极大的提高服务器端IO效率。Buffer、Channel和Selector共同实现了NIO的编程模式,其中Buffer也可以被独立的使用,用来完成缓冲区的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值