Buffer
是一块可读写的内存空间,Java NIO
里封装了许多方法用来访问这块内存,Buffer
常用于和 Channel
交互,数据从Channel
中读到Buffer
里 或者 从Buffer
写到Channel
中。
为什么是Buffer
和Channel
呢?我们可以这样理解:
点击查看【processon】
我们不需要关心Channel
另外一端的数据源,开发人员只需要拿着Buffer
从Channel
中接数据即可,或者把Buffer
中的数据倒入Channel
即可,具体Channel
连向哪里并不需要在这时候关心。
使用Buffer
时通常四步走:
- 将数据写入
Buffer
- 将
Buffer
从 写模式转 换为 读模式(参考flip()
) - 从
Buffer
中读取数据 - 将
Buffer
中的数据清空,准备接收新的一批数据。这里有两个方法:clear()
:清空整个Buffer
,一滴不剩,所有状态恢复到初始状态compact()
:将所有已读的数据清空掉,未读的数据移到Buffer
起始处,新写入的放在未读数据后。
代码示例如下所示:
public static void main(String[] args) throws IOException {
File file = new File("D:\\workspace_special\\present\\Java NIO\\src\\main\\resources\\data-file-nio.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel inChannel = randomAccessFile.getChannel();// 获取通道
ByteBuffer buffer = ByteBuffer.allocate(1024);
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
int readLen = inChannel.read(buffer);
while(readLen != -1) {
buffer.flip(); // 从写转换为读
while(buffer.hasRemaining()) {
System.out.println(decoder.decode(buffer));
}
buffer.clear(); // 清空buffer
readLen = inChannel.read(buffer);
}
inChannel.close();
randomAccessFile.close();
}
这里需要 ByteBuffer 转 中文( CharBuffer ),所以用到了 CharsetDecoder。 当数据量比较大时,一次性无法读完整个文本,可能会发生读取 一个字符的一部分字节(一个UTF8中文字符占用3个字节,可能因为 Buffer 快满了,只能读1个字节,导致只读了一个字符里的一个字节),此时会抛出
MalformedInputException
异常
Buffer结构剖析
Buffer
主要由四个部分组成:
- 一连串的内存空间,其大小为一开始
allocate(<CAPACITY>)
分配的大小 Capacity
为一开始分配的上限,就好比一个水桶在出厂时就决定了能够盛放多少的水。 我们在创建Buffer
时通过指定Capacity
从而指定了Buffer
的上限。Position
为当前操作的位置,如果在读模式下,就表示下一个要读的数据;如果在写模式下,就表示下一个要写入的位置。Limit
在写模式下和Capacity
相同,在读模式下Limit
就表示可以读取的上限了。Position
通常只和Limit
打交道,由Limit
决定Position
的可读上限、可写上限。
示意图如下所示:
点击查看【processon】
方便理解举一个现实的例子,就好比一个固定为 1000ML 的水桶,在倒水进去的阶段,我们至多倒1000ML
,倒了多少水进桶那么就会有多少水可以使用;若想把水从桶里倒出去,若前面接了 500ML 的水,那么也只能倒出 500ML 的水。
Buffer的分配
在讲分配这节时,我们需要了解一下 JVM 和 Java进程 的关系:
点击查看【processon】
Java
进程:由操作系统管理的程序,主要由代码段、数据段、堆空间(发生malloc()
动态分配时)、栈空间(函数调用、函数内的局部变量等等)组成。JVM
:是Java
进程 模拟出来的一个环境,咱们说的Java
程序是跑在JVM
虚拟机上的。
我们的Application
(Java程序)依赖于 JVM虚拟机 的环境,即堆空间、栈空间等等;而JVM虚拟机
依赖于操作系统提供的环境,即代码段、数据段、栈端等等。
堆内分配
ByteBuffer buf = ByteBuffer.allocate(48);
堆内分配就是在 JVM 的堆上进行分配,发生在JVM
中。
堆外分配
ByteBuffer buf = ByteBuffer.allocateDirect(48);
堆外分配则是在 Java进程 上进行分配,相当于在栈空间里用malloc()
分配了一段空间给JVM
使用。使用堆外分配的好处就是发生channel#write()
、channel#read()
时可以减少一次拷贝。具体点来说,JVM
里的程序想要把数据写入到磁盘上,需要经过以下几个步骤:
- 将 JVM 中的数据 拷贝 到 Java进程 中的空间(虽然都在同一个内存空间中,但是地址是不一样的,会有一个翻译的过程)
- Java进程 将数据拷贝到 内核空间
- 内核空间在适当的时候执行写入
Buffer#flip()
用于将Buffer
从 写入模式 切换到 读出模式,执行该方法后,limit = position
并且 position = 0
。此时position
表示下一个可读的位置,limit
表示可以读取多少数据(为上次写进了多少个byte
、char
等等,取决于具体使用的是ByteBuffer
还是CharBuffer
还是其他的什么)。
Buffer#hasRemaining()
返回Buffer
中是否还含有剩余的字节。判断规则为limit - position
是否大于0,如果大于0则返回 true
;反之返回 false
。
Buffer#rewind()
在 读出模式 下,可以将position
设置为 0,从而可以重新读取Buffer
中的所有(limit
不变,里面的数据不变)。
Buffer#clear() 和 Buffer#compact()
- 前者用于清空整个
Buffer
- 后者在 读出模式 下,可以将已经读出的数据清空,然后将未读的数据移动到队列前,新的数据继续追加到未读数据的后边
Buffer#mark() 和 Buffer#reset()
- 通过
mark()
可以标记一个位置 - 在执行了
mark()
之后,通过reset()
可以恢复到mark()
标记的位置
Buffer操作注意事项
- 如果
Buffer
已经满了,即position == capacity - 1
时,就需要清空Buffer
才能继续写入 position
最大为capacity - 1
- 当从写模式切换到读模式时,
limit = position; position = 0
。position
会被置为 0,此时position
表示下一个可读的位置
实战
public class FileNIOMain4Rewind {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
File file = new File("D:\\workspace_special\\present\\Java NIO\\src\\main\\resources\\number-file-nio.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel inChannel = randomAccessFile.getChannel();// 获取通道
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = inChannel.read(buffer);
printTip("写入了" + read + "个字节后:", buffer);
buffer.flip();
printTip("切换到读模式:", buffer);
readAnyMuch(buffer, 100);
printTip("在读取了100个字节后的position(从0开始算):", buffer);
// 已经消耗了100个了
buffer.rewind();
printTip("rewind()之后,此时的position:", buffer);
readAnyMuch(buffer, 300);
printTip("在读取了300个字节后(从0开始算):", buffer);
buffer.mark();
printTip("mark()后:", buffer);
readAnyMuch(buffer, 200);
printTip("在读取了200个字节后(从0开始算):", buffer);
buffer.reset();
printTip("reset()后:", buffer);
buffer.compact();
printTip("compact()后:", buffer); // 因为之前读了300个,还剩724个,此时position会停留在724上(0~723的位置上都有数据,指向724)
buffer.clear();
printTip("clear()后:", buffer);
inChannel.close();
}
private static void readAnyMuch(ByteBuffer buffer, int count){
for (int i = 0; i < count; i++) {
buffer.get();
}
}
private static void printTip(String prompt, ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
System.out.println(prompt + " 此时的position:" + getPosition(buffer) + ", limit:" + getLimit(buffer) + ", capacity:" + getCapacity(buffer));
}
private static int getPosition(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
Field positionField = Buffer.class.getDeclaredField("position");
positionField.setAccessible(true);
Integer position = (Integer) positionField.get(buffer);
return position;
}
private static int getLimit(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
Field positionField = Buffer.class.getDeclaredField("limit");
positionField.setAccessible(true);
Integer limit = (Integer) positionField.get(buffer);
return limit;
}
private static int getCapacity(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
Field positionField = Buffer.class.getDeclaredField("capacity");
positionField.setAccessible(true);
Integer capacity = (Integer) positionField.get(buffer);
return capacity;
}
}
写入了512个字节后: 此时的position:512, limit:1024, capacity:1024
切换到读模式: 此时的position:0, limit:512, capacity:1024
在读取了100个字节后的position(从0开始算): 此时的position:100, limit:512, capacity:1024
rewind()之后,此时的position: 此时的position:0, limit:512, capacity:1024
在读取了300个字节后(从0开始算): 此时的position:300, limit:512, capacity:1024
mark()后: 此时的position:300, limit:512, capacity:1024
在读取了200个字节后(从0开始算): 此时的position:500, limit:512, capacity:1024
reset()后: 此时的position:300, limit:512, capacity:1024
compact()后: 此时的position:212, limit:1024, capacity:1024
clear()后: 此时的position:0, limit:1024, capacity:1024