Java NIO 系列文章
- 高并发IO的底层原理及4种主要IO模型
- Buffer的4个属性及重要方法
Buffer类及其属性
Buffer类
Buffer
类是一个抽象类,对应于Java的主要数据类型,在NIO
中有7种缓冲区类
实际上,使用最多的还是ByteBuffer
二进制字节缓冲区类型
Buffer类的重要属性
Buffer
类在其内部,有一个相应类型的数组内存块,作为内存缓冲区。为了记录读写的状态和位置,Buffer
类提供了一些重要的属性。其中,有三个重要的成员属性:capacity
(容量)、position
(读写位置)、limit
(读写的限制)。
除此之外,还有一个标记属性:mark
(标记),可以将当前的position
临时存入mark
中;需要的时候,可以再从mark
标记恢复到position
位置。
capacity属性
Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity容量,缓冲区就满了,不能再写入了。
Buffer类的capacity属性一旦初始化,就不能再改变。因为Buffer类的对象在初始化时,会按照capacity分配内部的内存,内存分配好之后,它的大小当然不能再改变了。
前面讲到,Buffer类是一个抽象类,Java不能直接用来新建对象。使用的时候,必须使用Buffer的某个子类,例如使用DoubleBuffer,则写入的数据是double类型,如果其capacity是100,那么我们最多可以写入100个double数据
limit属性
Buffer类的limit属性,表示读写的最大上限。limit属性与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的。 + 写模式: limit表示可以写入的最大上限。在刚进入写模式时,limit会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满 + 读模式: limit的含义为最多能从缓冲区中读取到多少数据。
position属性
Buffer类的position属性,表示当前的位置。position属性与缓冲区的读写模式有关。在不同的模式下,position属性的值时不同的,当缓冲区进行读写模式切换时,position会进行调整。 + 写模式:position变化规则如下 1. 刚进入写模式时,position值为0,表示当前的写入位置为从头开始 2. 每当一个数据写到缓冲区后,position会向后移动到下一个可写位置,position+1 3. 初始position为0,最大可写值position为limit - 1 4. 当position值达到limit时,缓冲区就无空间可写了 + 读模式: position变化规则如下 1. 刚进入读模式时,position被重置为0,表示从头开始读 2. 读取数据后,position移动到下一个可读位置,position+1 3. position最大的值为最大可读上限limit,当position到达limit时,表明缓冲区已无数据可读
4个属性小结
除了前面的3个属性,第4个属性mark(标记)比较简单。就是相当一个暂存属性,暂时保存position的值,方便后面的重复使用position值。
Buffer类的重要方法
主要介绍Buffer类的几个重要方法,包含Buffer实例的获取、对Buffer实例的写入、读取、重复读、标记和重置等。
首先定义一个公共的打印类
private static void logger(IntBuffer intBuffer) {
System.out.println("capacity: " + intBuffer.capacity());
System.out.println("limit: " + intBuffer.limit());
System.out.println("position: " + intBuffer.position());
}
allocate()创建缓冲区
public static void allocateTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
logger(intBuffer);
}
例子中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(20),创建了一个Intbuffer实例对象,并且分配了20*4个字节的内存空间。
position:0
limit20
capacity20
从上面的运行结果,可以看出:
一个缓冲区在新建后,处于写入的模式,position写入位置为0,最大可写上限limit为的初始化值(这里是20),而缓冲区的容量capacity也是初始化值。
put()写入到缓冲区
public static void putTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
// 写4个
IntStream.range(1, 5).forEach(intBuffer::put);
logger(intBuffer);
}
输出结果如下:
capacity: 20
limit: 20
position: 4
从结果可以看到,position变成了4,指向了第5个可以写入的元素位置。而limit最大写入元素的上限、capacity最大容量的值,并没有发生变化。
flip()翻转
向缓冲区写入数据之后,是否可以直接从缓冲区中读取数据呢?呵呵,不能。
这时缓冲区还处于写模式,如果需要读取数据,还需要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读取模式。
public static void flipTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
// 写4个
IntStream.range(1, 5).forEach(intBuffer::put);
logger(intBuffer);
// 翻转缓冲区,从写模式翻转成读模式
intBuffer.flip();
logger(intBuffer);
}
输出结果如下:
capacity: 20
limit: 20
position: 4
capacity: 20
limit: 4
position: 0
调用flip方法后,之前写入模式下的position值4,变成了可读上限limit值4;而新的读取模式下的position值,简单粗暴地变成了0,表示从头开始读取。
对flip()方法的从写入到读取转换的规则,详细的介绍如下: 1. 首先,设置可读的长度上限limit。将写模式下的缓冲区中内容的最后写入位置position值,作为读模式下的limit上限值。 2. 其次,把读的起始位置position的值设为0,表示从头开始读。 3. 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置。在读模式下,如果继续使用旧的mark标记,会造成位置混乱
有关上面的三步,其实可以查看flip方法的源代码,Buffer.flip()方法的源代码如下:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
新的问题来了,在读取完成后,如何再一次将缓冲区切换成写入模式呢?
可以调用Buffer.clear()
清空或者Buffer.compact()
压缩方法,它们可以将缓冲区转换为写模式。
Buffer的模式转换,大致如下图所示:
get()从缓冲区读取
调用flip方法,将缓冲区切换成读取模式。这时,可以开始从缓冲区中进行数据读取了。读数据很简单,调用get方法,每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
public static void getTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
System.out.println("---> getTest start");
// 写 5 个
IntStream.range(1, 6).forEach(intBuffer::put);
logger(intBuffer);
// 翻转 写 --> 读
intBuffer.flip();
// 读 2 个
IntStream.range(1, 3).forEach(i -> System.out.println(intBuffer.get()));
logger(intBuffer);
// 读 3 个
IntStream.range(1, 4).forEach(i -> System.out.println(intBuffer.get()));
logger(intBuffer);
// 此时已经读完所有5个数据,能否立即进入 写模式呢?
// 答案是: 不能! 需要调用 Buffer.clear() 或 Buffer.compact() 即清空或压缩缓冲区 才能进入 写 模式
intBuffer.compact();
IntStream.range(1, 5).forEach(intBuffer::put);
logger(intBuffer);
}
输出结果如下:
// 写5个数据
capacity: 20
limit: 20
position: 5
// flip() 进入读模式
// 读 2个数据
1
2
capacity: 20
limit: 5
position: 2
// 读3个数据
3
4
5
capacity: 20
limit: 5
position: 5
从程序的输出结果,我们可以看到,读取操作会改变可读位置position的值,而limit值不会改变。如果position值和limit的值相等,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了。此时再读,会抛出BufferUnderflowException
异常。
这里强调一下,在读完之后,是否可以立即进行写入模式呢?不能。现在还处于读取模式,我们必须调用Buffer.clear()
或Buffer.compact()
,即清空或者压缩缓冲区,才能变成写入模式,让其重新可写。
rewind()倒带
已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。
public static void rewindTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
// 写5个数据
IntStream.range(1, 6).forEach(intBuffer::put);
// 切换到读模式
intBuffer.flip();
// 第一次读
IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));
logger(intBuffer);
// IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));
// 倒带 position -> 0
intBuffer.rewind();
logger(intBuffer);
// 第二次读
IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));
logger(intBuffer);
}
输出结果如下:
// 第一次读
1
2
3
4
5
capacity: 20
limit: 5
position: 5
// rewind() 倒带
capacity: 20
limit: 5
position: 0
// 第二次读
1
2
3
4
5
capacity: 20
limit: 5
position: 5
rewind()方法,主要是调整了缓冲区的position属性,具体的调整规则如下: 1. position重置为0,所以可以重读缓冲区中的所有数据。 2. limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取多少个元素。 3. mark标记被清理,表示之前的临时位置不能再用了。 Buffer.rewind()方法的源代码如下:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
mark()和reset()
Buffer.mark()方法的作用是将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark的值恢复到position中
public static void markAndResetTest() {
IntBuffer intBuffer = IntBuffer.allocate(20);
// 写5个
IntStream.range(1, 6).forEach(intBuffer::put);
// 切换到读
intBuffer.flip();
// 读3个,并在position为2时,做标记
IntStream.range(1, 4).forEach(i -> {
// 标记position
if (i == 2) {
intBuffer.mark();
}
System.out.println(intBuffer.get());
});
logger(intBuffer);
// 将position 重新设置成 mark
intBuffer.reset();
logger(intBuffer);
//重置后读
IntStream.range(1, 3).forEach(i -> {
System.out.println(intBuffer.get());
});
}
输出结果如下:
// 读3个
1
// mark做标记
2
3
capacity: 20
limit: 5
position: 3
// reset 重置
capacity: 20
limit: 5
position: 1
// 重置后读
2
3
clear( )清空缓冲区
在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法会将position清零,limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
使用Buffer类的基本步骤
总体来说,使用Java NIO Buffer类的基本步骤如下: 1. 使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。 2. 调用put方法,将数据写入到缓冲区中。 3. 写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。 4. 调用get方法,从缓冲区中读取数据。 5. 读取完成后,调用Buffer.clear()或Buffer.compact()方法,将缓冲区转换为写入模式。