一、缓冲区
缓冲区(Buffer)是一个用于存取特定基本数据类型数据的容器,底层是这些基本数据类型的数组,由java.nio包定义,所有缓冲区都是Buffer抽象类的子类。主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
根据数据类型不同,NIO提供了以下类型的Buffer:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
没有提供BooleanBuffer,其中最常用的是ByteBuffer,这些Buffer的使用方式几乎一样
二、Buffer的基本属性
在缓冲区的抽象基类Buffer中定义了缓冲区的四个基本属性:
public abstract class Buffer {
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}
①容量 (capacity) :表示 Buffer 的最大数据容量,缓冲区容量不能为负,并且创建后不能更改,其实就是指定底层数组的长度
② 限制 (limit):第一个不能读取或写入数据的索引位置,即位于 limit 及其之后的数组索引位置均不可读写。缓冲区的limit值不能为负,并且不能大于其容量
③位置 (position):下一个要读取或写入数据的索引位置。缓冲区的位置不能为负,并且不能大于其限制limit值
④标记 (mark) 与重置 (reset):标记的是一个索引位置,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position 再次进行读写
标记、位置、限制、容量遵守以下规则:
0 <= mark <= position <= limit <= capacity
三、Buffer的数据存取
Buffer通过两个核心方法存取数据:put()和get()都有很多重载方法
put();//存入数据到缓冲区
get();//从缓冲区获取数据
1、Buffer数据存取的示例:以ByteBuffer为例
public void testBuffer() {
String str = "abcde";
// 使用allocate()创建指定容量的buffer
ByteBuffer buf = ByteBuffer.allocate(10);
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 10
System.out.println(buf.position());// 0
// 向创建的buffer中存入数据
buf.put(str.getBytes());
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 10
System.out.println(buf.position());// 5,创建的是byte类型的buffer,一个位置只能存放一个字符,因此position值变为5
buf.flip();// 执行该方法后limit值变为原position值,position值变为0,即指定可操作的范围为0-position,此处即读的范围为0-5
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 5
System.out.println(buf.position());// 0
byte[] dst = new byte[buf.limit()];
buf.get(dst);// 使用get()将缓冲区中的数据写入到指定的byte[]
System.out.println(new String(dst, 0, dst.length));// abcde
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 5
System.out.println(buf.position());// 5
buf.rewind();// 将position的值置为0,可通过该方法再重头开始读数据
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 5
System.out.println(buf.position());// 0
buf.clear();// 仅仅是将limit的值重置为capacity,将position的值重置为0,这样就可以从头开始写数据,但并没有清空buffer中的数据
System.out.println(buf.capacity());// 10
System.out.println(buf.limit());// 10
System.out.println(buf.position());// 0
System.out.println((char) buf.get());// a,buffer中的数据依然能够读取出来,get()是获取当前position位置的值
}
2、mark和reset的使用示例:
public void testBuffer1() {
String str = "abcde";
// 使用allocate()创建指定容量的buffer
ByteBuffer buf = ByteBuffer.allocate(10);
// 向创建的buffer中存入数据
buf.put(str.getBytes());
buf.flip();// limit值变为原position值,position值变为0
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);// 从buffer中的当前position位置往后读2个字符写入dst中,写入dst的起始索引为0
System.out.println(new String(dst, 0, 2));// ab
System.out.println(buf.position());// 2
buf.mark();// 记录下此时position的位置:2
buf.get(dst, 1, 2);//从buffer中的当前position位置往后读2个字符写入dst中,写入dst的起始索引为1
System.out.println(new String(dst, 0, dst.length));//acd
System.out.println(buf.position());// 4
buf.reset();// 将position的值恢复至mark时的值
System.out.println(buf.position());// 2
}
注意:Buffer中的get方法的第2个参数offset指的是从目标数组的第offset个位置写入数据到目标数组,第3个参数length指的则是从当前buffer的当前position位置向后读取的buffer中的数据的长度,即offset和length针对的主体是不同的
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
四、Buffer中的常用操作
方法 | 描述 |
---|---|
Buffer clear() | 将缓冲区的界限设置为buffer的容量值,并将当前位置置为 0,返回对缓冲区的引用,但并不清除缓冲区中的数据 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置重置为 0 |
int capacity() | 返回 Buffer 的 capacity 大小 |
boolean hasRemaining() | 判断缓冲区中是否还有元素 |
int limit() | 返回 Buffer 的界限(limit) 的位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置 position |
Buffer position(int n) | 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 |
int remaining() | 返回 position 和 limit 之间的元素个数 |
Buffer reset() | 将位置 position 转到以前设置的 mark 所在的位置 |
Buffer rewind() | 将位置设置为 0, 并取消设置的 mark |
put(byte b) | 将给定单个字节写入缓冲区的当前位置 |
put(byte[] src) | 从当前位置开始将 src 中的字节写入缓冲区 |
put(int index, byte b) | 将指定字节写入缓冲区的指定索引位置(不会移动 position) |
get() | 读取单个字节 |
get(byte[] dst) | 批量读取多个字节到 dst 中 |
get(int index) | 读取指定索引位置的字节(不会移动 position) |
五、直接缓冲区和非直接缓冲区
非直接缓冲区:通过allocate()方法分配的缓冲区,将缓冲区建立在JVM的内存中
直接缓冲区:通过allocateDirect()方法分配的缓冲区,将缓冲区直接建立在物理内存中,可以提高数据操作的效率
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前或之后, 虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
通过调用 allocateDirect() 方法来创建直接缓冲区,此方法返回的缓冲区在进行分配和取消分配时所需的成本通常高于创建非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区分配给那些易受基础系统本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法将文件区域直接映射到内存中来创建。该方法返回 MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来判定。提供此方法是为了能够在性能关键型代码中执行显式的缓冲区类型管理。
非直接缓冲区数据操作示意图:
读数据时:应用程序先在JVM(即用户地址空间)中创建一个缓冲区,然后JVM通知操作系统要读取磁盘中的文件数据,同时操作系统需要在物理内存(即内核地址空间)中创建一个缓冲区用以接收从磁盘读取的文件,在操作系统完成文件读取后再将物理内存中的文件数据复制(copy)到JVM(用户地址空间)的缓冲区中,应用程序再从JVM中读取数据
写数据时:应用程序先在JVM(即用户地址空间)中创建一个缓冲区,然后将数据写入到JVM中的缓冲区,再通知操作系统将JVM中的数据写入磁盘,这时操作系统需要在物理内存(即内核地址空间)中创建一个缓冲区用以存储从JVM读取的文件,在物理内存中的缓冲区创建好之后,再将JVM中的缓冲区中的数据复制到物理内存中的缓冲区,最后将物理内存中的缓冲区中的数据写入磁盘
可以看到,无论是读还是写数据,都存在复制数据的步骤,直接缓冲区就是应用程序通过直接将缓冲区建立在操作系统的物理内存上而将这一步骤省略了
直接缓冲区数据操作示意图:
这样在读写数据时,应用程序直接读写物理内存,物理内存中的数据直接写入磁盘或者从磁盘接收数据。
创建直接缓冲区:
public void testDirectBuffer() {
//创建直接缓冲区
ByteBuffer buf = ByteBuffer.allocateDirect(10);
//判断缓冲区是否为直接缓冲区
System.out.println(buf.isDirect());
}