浅析Java Nio 之缓冲区

浅析Java Nio 之缓冲区


缓存区

缓冲区及相应工作是IO的基础,输入和输出可以简单地看做是把将数据从缓冲区移入或者移出。
进程进行IO操作,就是通过通知操作系统,对缓冲区中的数据进行移出(写),或者把数据填充到缓冲区(读)。进程通过read的系统调用,将缓冲区填满。首先内核向磁盘控制硬件发出命令,对磁盘读取数据,磁盘控制器通过DMA将数据从来磁盘直接读入内核内存缓冲区。当缓冲区数据读满后,内核会把数据从内核空间的临时缓冲区拷贝到read系统调用指定的缓冲区。
缓冲区是一个固定数量的数据容器,它可以看做一个数据存储器,存储的数据可以用于检索和操作。

缓冲区属性

  • 容量(Capacity)
    在缓存区被构建的时候设置,一旦设置就不能改变,它指定了缓冲区能够存储数据的最大容量。
  • 上界(Limit)
    缓冲区中现有元素的个数,它表明了缓冲区第一个不能被读或者写的元素。
  • 位置(Position)
    缓冲区下一个要被读取或者写入的索引位置,会通过get()或者put()方法进行更新。
  • 标志(Mark)
    备忘位置,他可以使用mark方法来设定mark = position,或者使用reset()来设定position =mark.当mark未被设定之前为-1。

mark <= position <= limit <= capacity

缓冲区基础操作

  • 存取
    缓冲区存储着固定数目的数据元素,我们可以对缓存区进行存取操作,需要能够知道当前缓冲区中的数据元素的数量来确定存储下一个元素的对应索引位置,buffer通过Position来实现的。由于buffer存储的元素采取的数据类型并不一样,get和put方法并没有在父类中抽象声明,而是根据不同的子类中进行实现。get/put方法可以是相对或者绝对的,当相对存取方法被调用的时候,position会在返回的时候进行+1操作,绝对存取方法调用的时候,不会影响position属性。在进行get/put方法如果越界,会抛出相应的异常。

  • 翻转
    当缓存区填充了之后,如果我们调用get方法对数据内容进行输出时,它取出的是在最新存入数据的位置索引外的未定义数据,如果我们想要从正确的位置取出数据,需要把上届limit设置为当前的position,同时把position归0,这样就可以正确把一个等待数据存入的缓冲区翻转为一个输出数据的缓冲区。同时buffer提供了flip方法帮助我们封装了以上的操作。而rewind方法跟flip方法类似,不过rewind方法不会修改上界的值,只是把position设为0。

  • 释放
    如果缓冲区填充满了,我们对释放缓冲区进行重用。我们可以通过remaining方法得到从当前位置到上界的元素数目或者通过hasRemaining方法判断缓冲区是是否达到上界。clear方法会将缓冲区重置,它不改变缓冲区中的数据元素,它将缓冲区的limit设为capacity的值,然后把position设为0.

  • 压缩
    有时候我们会只是从缓冲区中释放一部分数据,而不是全部数据,然后进行填充,我们需要对缓冲区进行compact,compact会将还没有被读取的元素下移直到还没有被读取的第一个元素的索引位置为0,然后将position设为当前未读元素的数目和将limit设为缓冲区的capacity,因此超出修改后的position之后的元素可以看成是‘死的’,它们不可以被get,但可以调用put方法进行重写。compact主要是丢弃已经释放的数据,保留还没有释放的数据,并且对缓冲区的重新填充作准备。

  • 标志
    mark可以让缓冲区能够记住缓冲区当前的position并在之后返回,在mark方法调用之前,mark的值为-1。reset方法可以将position设置为之前mark的值。如果之前没有调用mark会抛出InvalidMarkException。如果调用rewind,clear,flip方法会导致mark的值被重新设为-1,或者调用limit(newLimit)和position(newPosition)方法,新值比mark的值小的话,也会导致mark的值重新被设为-1.

  • 比较
    buffer提供了equals来判断两个缓冲区是否相等和compareTo方法来对缓冲区进行比较。判断两个缓冲区是否equals不需要它们的属性相同,只需要buffer类型相同、剩余相同的元素数目(不要求容量、元素索引相同,只要求从位置到上界的数目)及返回的元素序列相等则可以看做两个缓冲区相等,否则不等。compareTo方法比equals方法更为严格,如果buffer类型不同,会抛出ClasscastException异常。compareTo返回负整数、0、正整数,因此Arrays.sort函数对缓冲区进行排序。

  • 批量移动
    为了高效传输数据,buffer还提供了批量移动方法。buffer提供两种形式的get方法从缓冲区到数组进行的数据复制时使用。get可以传入数组作为参数,将缓冲区释放到数组,还可以传入offset和length参数来指定目标的子区间。如果要求传输的数据数目不能被传输,那么就不会有数据被传输,缓冲区状态保持不变,同时抛出BufferUnderflowException异常。因此如果传入一个数组没有指定offset和length,就相当于整个数组被填充,那么如果缓冲区的数据不能够完成填充满数组,就会抛出异常。put方法的批量版本跟get方法差不多,但以相反的方向移动数据,从数组移动缓冲区。put方法要求缓冲区中有足够的空间接收数组中的数据,数据将会被复制到当前位置开始的缓冲区,并且缓冲区位置会被提前所增加的数据元素的数量。如果缓冲区没有足够的空间,就不会有数据被传输,同时抛出BufferOverflowException。

创建缓存区

根据JAVA除了boolean类型的基本类型,缓冲区也有七种对应的缓冲区类型,但是由于他们都是抽象类,不可以直接被实例化。一般是通过对应缓冲区类的静态工厂方法来创建类的实例。缓冲区主要是通过allocate和wrap方法来创建的。allocate会创建一个缓冲区并分配一定的空间来存储指定大小容量的数据元素。wrap会创建一个缓冲区但是不会分配空间来存储数据元素,它是利用传入的数组来作为存储空间来储存缓冲区中的数据的,这样的话,如果对缓冲区进行put操作的话,也会对数组进行改变,同样对数组的改变也会对缓存区对象可见的。wrap(array,offest,length)并不是创建了一个只占有数组子集的缓冲区,这个缓冲区可以存取整个数组的全部范围,offest和length只是设置了初始状态,可以调用clear方法,对缓冲区进行重新填充,直到超过上界的值,对数组中元素进行重写。allocate和wrap方法创建的缓冲区都是间接的,间接的缓冲区使用备份数组,可以通过hasArray方法来判断缓冲区是否含有一个可存取的备份数组,如果是true的话,可以调用array方法返回缓冲区对象的备份数组的引用。如果缓冲区是只读的话,如果调用array方法的话,会抛出ReadOnlyException异常来阻止来获取备份数组来对只读缓冲区的数据进行修改。

复制缓冲区

我们不仅可以通过传入数组对象来创建缓冲区对象,也可以管理其他缓冲区的数据。当一个管理其他缓冲区的数据元素的缓冲区的缓冲区被创建的时候,这个缓冲器被称为视图缓冲器。
duplicate方法会创建一个新的缓冲区来共享旧缓冲区的数据,拥有相同的容量,而对其中一个缓冲区的数据进行改变对另一个缓冲区可见,两个缓冲区的limit,position,mark的值是相互独立的。如果旧缓冲区是直接或者只读的话,新缓冲区也会是直接或者只读的。
asReadOnlyBuffer方法会创建一个只读的缓冲区视图,新的缓冲区不允许调用put方法,一旦调用会抛出ReadOnlyBufferException。如果是一个只读的缓冲区与一个可写的缓冲区共享数据,可写缓冲区的改变都会对所有关联的缓冲区可见,包括只读缓冲区。
Slice方法会创建一个从原缓冲区的position开始的缓冲区,容量为原缓冲区的剩余数据元素数目,新的缓冲区会与原缓冲区共享同一段数据元素,同时继承原缓冲区的只读和直接属性。

字节缓冲区

  • 字节顺序
    每一个基本数据类型都是通过连续字节序列的形式存储在内存中。字节顺序分为大端字节顺序和小端字节顺序。ByteBuffer默认字节顺序为ByteBuffer.BIG_ENDING,保证类文件或者串行化的对象可以在任何JVM中工作,ByteBuffer的字节顺序可以通过order方法来进行设定。
  • 直接缓冲区
    ByteBuffer是channel所执行IO的源头或者目标,IO操作的目标内存区域必须是连续的字节序列,由于字节数组在JAVA中是一个对象,不一定会在内存当中连续存储,数据存储在对象中的方式在不同JVM实现中会有不同,因此只有字节缓冲区才能参与IO操作。
    直接字节缓冲区可用于通道和固有IO的例程交互,通过固有代码来通知操作系统直接释放或者填充内存区域。非直接字节缓冲区可以传递给通道,但是导致性能损耗。向通道传入一个非直接的ByteBuffer用于写入的时候,通道会创建一个临时的直接ByteBuffer对象,然后将非直接缓冲区的数据复制到临时缓冲区中,使用临时缓冲区进行IO操作,IO操作结束之后,临时缓冲区离开作用域,成为被回收的无用数据。这些非直接缓冲区进行io操作的时候,可能会进行复制和产生大量的对象,因此如果需要进行大量IO操作的时候,请分配直接字节缓冲区。直接ByteBuffer可以通过ByteBuffer静态方法allocateDirect()方法创建。
  • 视图缓冲区
    当缓冲区接收到数据后,我们可以通过创建视图缓冲区来对查看或者在读取数据之前对其进行操作。视图缓冲区可以通过已经存在的缓冲区调用工厂方法来创建,视图缓冲区会维护自己的limit,position,mark,capacity属性,但是会与原缓冲区共享数据元素。ByteBuffer缓冲区可以将缓冲区中数据映射成其他的原始数据类型的视图缓冲区。视图缓冲区的容量是根据原字节缓冲区的数据元素数目除以映射缓冲区的数据类型的字节数来确定的。
  • 数据元素视图
    ByteBuffer提供以多字节数据类型的形式来存取byte数组。ByteBuffer会从当前位置根据有效的字节顺序(大端字节顺序或者小端字节顺序)来排序或者打乱组成相应的数据类型,如果试图去获取的数据类型需要比缓冲区存在的字节数更多的字节的话,会抛出ByteBufferUnderflowException。get方法从缓冲区的当前位置开始的字节缓冲区中取出并组合,它允许对一个字节流中数据进行随机的放置。put方法的话会对基本类型数据的值根据字节顺序拆分为一个个byte数据进行存储,如果存储的数据的空间不够,会抛出BufferOverflowException。
  • 存取无符号数据
    JAVA没有对无符号数值提供直接的支持,但当我们需要将无符号的数据信息进行转换的数据流或者文件,需要自己去处理。
  • 内存映射缓冲区
    映射缓冲区是指存储在文件,通过内存映射来存取数据元素的字节缓冲区。映射缓冲区是直接存取内存的,只能通过FileChannel类创建。
原文地址:https://www.cnblogs.com/xianyijun/p/5378245.html