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(); // 清空bufferreadLen = 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
