Buffer是一块可读写的内存空间,Java NIO里封装了许多方法用来访问这块内存,Buffer 常用于和 Channel 交互,数据从Channel中读到Buffer里 或者 从Buffer写到Channel中。
为什么是BufferChannel呢?我们可以这样理解:
点击查看【processon】
我们不需要关心Channel另外一端的数据源,开发人员只需要拿着BufferChannel中接数据即可,或者把Buffer中的数据倒入Channel即可,具体Channel连向哪里并不需要在这时候关心。

使用Buffer时通常四步走:

  1. 将数据写入Buffer
  2. Buffer从 写模式转 换为 读模式(参考flip()
  3. Buffer中读取数据
  4. Buffer中的数据清空,准备接收新的一批数据。这里有两个方法:
    1. clear():清空整个Buffer,一滴不剩,所有状态恢复到初始状态
    2. compact():将所有已读的数据清空掉,未读的数据移到Buffer起始处,新写入的放在未读数据后。

代码示例如下所示:

  1. public static void main(String[] args) throws IOException {
  2. File file = new File("D:\\workspace_special\\present\\Java NIO\\src\\main\\resources\\data-file-nio.txt");
  3. RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  4. FileChannel inChannel = randomAccessFile.getChannel();// 获取通道
  5. ByteBuffer buffer = ByteBuffer.allocate(1024);
  6. CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
  7. int readLen = inChannel.read(buffer);
  8. while(readLen != -1) {
  9. buffer.flip(); // 从写转换为读
  10. while(buffer.hasRemaining()) {
  11. System.out.println(decoder.decode(buffer));
  12. }
  13. buffer.clear(); // 清空buffer
  14. readLen = inChannel.read(buffer);
  15. }
  16. inChannel.close();
  17. randomAccessFile.close();
  18. }

这里需要 ByteBuffer 转 中文( CharBuffer ),所以用到了 CharsetDecoder。 当数据量比较大时,一次性无法读完整个文本,可能会发生读取 一个字符的一部分字节(一个UTF8中文字符占用3个字节,可能因为 Buffer 快满了,只能读1个字节,导致只读了一个字符里的一个字节),此时会抛出 MalformedInputException异常

Buffer结构剖析

Buffer主要由四个部分组成:

  1. 一连串的内存空间,其大小为一开始allocate(<CAPACITY>)分配的大小
  2. Capacity为一开始分配的上限,就好比一个水桶在出厂时就决定了能够盛放多少的水。 我们在创建Buffer时通过指定Capacity从而指定了Buffer的上限。
  3. Position为当前操作的位置,如果在读模式下,就表示下一个要读的数据;如果在写模式下,就表示下一个要写入的位置。
  4. Limit在写模式下和Capacity相同,在读模式下Limit就表示可以读取的上限了。Position 通常只和 Limit打交道,由Limit决定Position的可读上限、可写上限。

示意图如下所示:
点击查看【processon】
方便理解举一个现实的例子,就好比一个固定为 1000ML 的水桶,在倒水进去的阶段,我们至多倒1000ML,倒了多少水进桶那么就会有多少水可以使用;若想把水从桶里倒出去,若前面接了 500ML 的水,那么也只能倒出 500ML 的水。

Buffer的分配

在讲分配这节时,我们需要了解一下 JVMJava进程 的关系:
点击查看【processon】

  • Java进程:由操作系统管理的程序,主要由代码段、数据段、堆空间(发生malloc()动态分配时)、栈空间(函数调用、函数内的局部变量等等)组成。
  • JVM:是 Java进程 模拟出来的一个环境,咱们说的Java程序是跑在JVM虚拟机上的。

我们的Application(Java程序)依赖于 JVM虚拟机 的环境,即堆空间、栈空间等等;而JVM虚拟机依赖于操作系统提供的环境,即代码段、数据段、栈端等等。

堆内分配

  1. ByteBuffer buf = ByteBuffer.allocate(48);

堆内分配就是在 JVM 的堆上进行分配,发生在JVM中。

堆外分配

  1. ByteBuffer buf = ByteBuffer.allocateDirect(48);

堆外分配则是在 Java进程 上进行分配,相当于在栈空间里用malloc()分配了一段空间给JVM使用。使用堆外分配的好处就是发生channel#write()channel#read()时可以减少一次拷贝。具体点来说,JVM里的程序想要把数据写入到磁盘上,需要经过以下几个步骤:

  1. 将 JVM 中的数据 拷贝 到 Java进程 中的空间(虽然都在同一个内存空间中,但是地址是不一样的,会有一个翻译的过程)
  2. Java进程 将数据拷贝到 内核空间
  3. 内核空间在适当的时候执行写入

Buffer#flip()

用于将Buffer写入模式 切换到 读出模式,执行该方法后,limit = position 并且 position = 0。此时position表示下一个可读的位置,limit表示可以读取多少数据(为上次写进了多少个bytechar等等,取决于具体使用的是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操作注意事项

  1. 如果Buffer已经满了,即position == capacity - 1 时,就需要清空Buffer才能继续写入
  2. position最大为capacity - 1
  3. 当从写模式切换到读模式时,limit = position; position = 0position会被置为 0,此时position表示下一个可读的位置

实战

  1. public class FileNIOMain4Rewind {
  2. public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
  3. File file = new File("D:\\workspace_special\\present\\Java NIO\\src\\main\\resources\\number-file-nio.txt");
  4. RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  5. FileChannel inChannel = randomAccessFile.getChannel();// 获取通道
  6. ByteBuffer buffer = ByteBuffer.allocate(1024);
  7. int read = inChannel.read(buffer);
  8. printTip("写入了" + read + "个字节后:", buffer);
  9. buffer.flip();
  10. printTip("切换到读模式:", buffer);
  11. readAnyMuch(buffer, 100);
  12. printTip("在读取了100个字节后的position(从0开始算):", buffer);
  13. // 已经消耗了100个了
  14. buffer.rewind();
  15. printTip("rewind()之后,此时的position:", buffer);
  16. readAnyMuch(buffer, 300);
  17. printTip("在读取了300个字节后(从0开始算):", buffer);
  18. buffer.mark();
  19. printTip("mark()后:", buffer);
  20. readAnyMuch(buffer, 200);
  21. printTip("在读取了200个字节后(从0开始算):", buffer);
  22. buffer.reset();
  23. printTip("reset()后:", buffer);
  24. buffer.compact();
  25. printTip("compact()后:", buffer); // 因为之前读了300个,还剩724个,此时position会停留在724上(0~723的位置上都有数据,指向724)
  26. buffer.clear();
  27. printTip("clear()后:", buffer);
  28. inChannel.close();
  29. }
  30. private static void readAnyMuch(ByteBuffer buffer, int count){
  31. for (int i = 0; i < count; i++) {
  32. buffer.get();
  33. }
  34. }
  35. private static void printTip(String prompt, ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
  36. System.out.println(prompt + " 此时的position:" + getPosition(buffer) + ", limit:" + getLimit(buffer) + ", capacity:" + getCapacity(buffer));
  37. }
  38. private static int getPosition(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
  39. Field positionField = Buffer.class.getDeclaredField("position");
  40. positionField.setAccessible(true);
  41. Integer position = (Integer) positionField.get(buffer);
  42. return position;
  43. }
  44. private static int getLimit(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
  45. Field positionField = Buffer.class.getDeclaredField("limit");
  46. positionField.setAccessible(true);
  47. Integer limit = (Integer) positionField.get(buffer);
  48. return limit;
  49. }
  50. private static int getCapacity(ByteBuffer buffer) throws NoSuchFieldException, IllegalAccessException {
  51. Field positionField = Buffer.class.getDeclaredField("capacity");
  52. positionField.setAccessible(true);
  53. Integer capacity = (Integer) positionField.get(buffer);
  54. return capacity;
  55. }
  56. }
写入了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