Java NIO中的Buffer

简介

  Buffer缓冲区,首先要弄明白的是,缓冲区是怎样一个概念。Buffer是把数据保存在内存中,它本质上用来保存数据的数据结构是数组,例如ByteBuffer是byte数组,IntBuffer是int数组等,对Buffer读写操作,其实是对该数组进行数据存放、读取、清除操作。

  Buffer可以理解成是一个容器,可以往容器里写入数据,也可以从容器中读取数据,可以一个字节一个字节的读写,也可以多个字节读写,可以在写的途中切换为读,读到一半又切换到写,等等。。。

  Buffer有以下几个属性:

  • int mark 标记
  • int position 位置
  • int limit限制
  • int capacity缓冲区的容量,Buffer的容量是固定的,在创建Buffer对象时,制定容量大小。

      这几个属性,有什么作用,用在什么地方,为何设置这4个属性。其实不用先理解,在了解reset(),flip()等方法后,就会明白这四个属性的用处。

主要方法

mark、reset

  • mark()在Buffer当前的位置设置标记

    1
    2
    3
    4
    public final Buffer mark() {
    mark = position;
    return this;
    }
  • reset()重置Buffer当前的位置为上一次的标记

    1
    2
    3
    4
    5
    6
    7
    public final Buffer reset() {
    int m = mark;
    if (m < 0)
    throw new InvalidMarkException();
    position = m;
    return this;
    }

  这两个方法的使用场景,从Buffer读取n个字节数据,结果读出来的数据长度比n小(比如网络是拆包问题),这时候会重新再读一次,调用reset()方法。但并不是所有读操作是从位置0开始读,所以在reset()先设置一个标记,调用mark(),最后position = mark,就是从标记的mark位置开始读。

clear,flip, rewind

  • clear()清除Buffer
    1
    2
    3
    4
    5
    6
    public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
    }
  • flip()反转Buffer,何谓“反转”?在执行完写操作之后,调用调用flip()为下次的读操作准备。
    1
    2
    3
    4
    5
    6
    public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
    }
      使用场景:在进行完写操作后,写入100个字节,所以当前位置position=100,为了下次读操作,读出来是这100个字节数据,所以limit也设为100,限制最多读到100个字节。下次读是从位置0开始,所以position=0。
    1
    2
    3
    4
    5
    6
    7
    ByteBuffer buffer = ByteBuffer.allocate(100);
    String value = "zhangxh20";
    buffer.put(value.getBytes());
    buffer.flip();
    byte [] arr = new byte[buffer.remaining()];
    buffer.get(arr);
    String decodeValue = new String(arr);
      看下调用flip()操作前后的对比。
    flip()之前
      如果不做flip操作,读到的将是position到capacity之间的错误内容。
    flip()操作之后
    当执行flip()操作之后,它的limit被设置为position,position设置为0,capacity不变,由于读取的内容是从position到limit之间,因此,他能够正确读取到之前写入缓冲区的内容。
  • rewind()重绕Buffer,可以发现rewind()flip()少了个步骤limit = positon。
    1
    2
    3
    4
    5
    public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
    }
      使用场景:buffer.get(array),把buffer的数据复制到array数组里,会调用buffer的get()方法。get()是从position开始复制,最多limit个,所以只需positon设置为0。

不变式
标记、位置、限制和容量值遵守以下不变式:

0 <= 标记 <= 位置 <= 限制 <= 容量
新创建的缓冲区总有一个 0 位置和一个未定义的标记。初始限制可以为 0,也可以为其他值,这取决于缓冲区类型及其构建方式。一般情况下,缓冲区的初始内容是未定义的。

线程安全

  Buffer不是线程安全的,被多个线程使用时,需要加同步。

ByteBuffer

  ByteBuffer字节缓冲区,即每次调用get()或者put()操作的数据最小单位是byte,理IntBuffer、LongBuffer、DoubleBuffer以此类推。例如LongBuffer put()最小的单位是8个字节的long数据。日常开发中,ByteBuffer使用的会比较多,其他类型Buffer很少使用。
  事实上ByteBuffer也可以用来表示其他类型Buffer,比如IntBuffer的get(),可以用ByteBuffer的getInt(),两个实现效果一样。

堆内存和直接内存

  Buffer在创建时,必须指定容量大小,还有分配内存的类型,有两种堆内存和直接内存,直接可以理解为堆外的内存空间,区别就是,直接内存是不受GC管理

参考资料

  • 《Netty权威指南》