Buffer

Posted 怀瑾握瑜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Buffer相关的知识,希望对你有一定的参考价值。

前言

上一篇文章中Java NIO概括性的介绍了Java Nio以及各个核心组件。这篇继续Java Nio的话题,着重了解下Nio中Buffer的原理、Buffer的行为、Buffer种类。

  • Buffer原理
  • Buffer行为
  • Buffer分类

Buffer原理

上篇中简书了Buffer是数据容器,可以重复读写。那么Buffer是如何存储数据的呢?Buffer的读写操作是如何标记读写的位置呢?带着这些问题,我们一起先来看下Buffer的存储,以及属性标记。

线性数组是一门最基础的数据结构,对很多数据结构提供的基本的存储支持。Buffer中使用了线性数组作为其存储实现,即一块连续的存储空间。
Buffer中有四个非常重要的属性位:

  • capacity: 表示Buffer的容量;
  • limit: Buffer的读写限制,表示最大能读写到limit的位置;
  • position: 记录下一个读写的位置;
  • mark::记录打标记的位置;

还是一如既往,先直观地图解Buffer

如上图,是一个容量为n的Buffer的初始化状态。创建Buffer时:

  1. 首先分配容量为n的数组,capacity为n,此时limit设置为n,表示最大的可写限制为n;
  2. position初始为0,表示即将写入的位置是槽位0
  3. mark初始化为-1,初始化的Buffer无标记位

Buffer中有几个非常重要的操作会导致Buffer的属性位发生变化

  • flip: 翻转Buffer
  • clear:清理Buffer
  • mark:标记Buffer

属性位的变化表示Buffer的状态切换,每种状态的Buffer都对应Buffer可操作的相应行为

上图展示了Buffer随着flip、clear、mark操作的属性变更和状态转移:初始化状态 -> 一次put后 -> flip后 -> 一个get后

两次put后,position移动两位,即position=2,表示下一个写入的位置,limit等于capacity表示Buffer最大支持的写入限制。
flip翻转主要是在Buffer的读写之间进行切换,flip后,将limit设置为position的大小,position归零,从0位开始读,读到limit为止。
一次get后,则将position移动一位,表示下一个即将读的位置。

从上可以看出,相应的操作导致属性位变化,Buffer的可读可写状态之间切换。Buffer基于此实现读写操作。

Buffer的mark用来给Buffer当前的postion位置打标记,即mark记录当前postion位置,当Buffer发生reset重置时,将postion设置为之前的mark位置,主要用来实现重复读写,持续读写。(复位代表着可持续操作!!)

Buffer行为

上一节中详细说明了Buffer的存储,读写原理。这一节中继续了解Buffer的常用行为。这里以字节缓冲区ByteBuffer(存储字节数据)的作为例子进行说明。
ByteBuffer的常用行为无非创建、读写,但是ByteBuffer对于读写需要遵循上述的flip,clear等操作。

1)ByteBuffer创建主要通过静态allocate方法实现(工厂模式)

ByteBuffer buffer = ByteBffer.allocate(16);

这里创建大小为16个字节容量的Buffer。

2)读操作主要通过get和get的变体方法实现,字节get方法主要有四种形式:

  • get():获取当前position槽位的值
  • get(byte[] b):从当前position获取内容至数组b中,直到填满数组。如果buffer数据不足,会抛出BufferUnderflowException
  • get(byte b[], int offset, int length) 从当前position获取内容至数组b中,从数组的偏移offset位开始填充,共获取length个
  • get(int index) 获取指定槽位的byte,position不发生偏移

3)写操作:主要通过put和put的变体方法实现,字节put方法主要有五种形式:

  • put(byte b):向当前position槽位写入一个字节
  • put(byte[] bs):从当前position槽位开始写入字节数组,如果buffer容量不足,则抛出:BufferUnderflowException
  • put(byte[] bs, int offset, int length) 从当前position槽位写入字节数组,从数组的offset偏移位置取,共写入length个
  • put(int index, byte b) 向指定index槽位写入数据
  • put(ByteBuffer buffer) 转换参数ByteBuffer

ByteBuffer除了基本的字节读写操作,还提供了更便利的get变体操作,完成以不同数据类型读写ByteBuffer,如:

  • getChar 以字符形式读取ByteBuffer
  • getInt 以int形式读取ByteBuffer
  • putChar 以字符形式写入Buffer
  • putInt 以int形式写入Buffer

除了以上的变体操作,还有很多其他的数据类型的get/put变体操作,可以查看ByteBuffer详细的api。

ByteBuffer除了基本的读写操作,还有几个继承自Buffer父类的行为,Buffer的行为在其派生出的子类中都具有:

  • isDirect:是否为直接Buffer(后续文章中会说明直接的含义)
  • isReadOnly:是否为只读
  • hasRemaining:是否存在剩余容量
  • remaining:剩余容量大小

Buffer分类

在以ByteBuffer为例介绍完,Buffer的常用行为后,下面在简单的了解下Nio中提供的Buffer类型,以及与ByteBuffer的区别。

Nio中的Buffer可以从多维角度进行划分:1.数据类型;2.堆Buffer和直接内存Buffer;3.抽想Buffer和其对应的具体实现

1) 数据类型
按照存储的数据类型,可以将Buffer划分为:

  • ByteBuffer: 存储字节
  • ShortBuffer:存储短整型
  • IntBuffer:存储整形
  • LongBuffer:存储长整型
  • CharBuffer:存储字符类型
  • FloatBuffer:存储浮点类型
  • DoubleBuffer:存储双精度

ByteBuffer提供get/put的变体,以方便的api实现其他数据类型的读写,其他类型的Buffer均只有基本数据类型的get/put读写。

2) 堆/直接内存
按照数据的存储位置,可以将Buffer分为堆Buffer和直接内存Buffer。

  • 堆Buffer:Buffer内存分配在Java内存的堆区域,Heap Buffer实现:
    HeapByteBuffer/HeapCharBuffer等

  • 直接内存Buffer:Buffer使用堆外的直接内存,如:Direct Buffer实现:
    DirectByteBuffer/DirectCharBuffer

3) 抽象Buffer和其对应实现
Nio中Buffer的设计模式非常强,工厂模式、。。。我回头再温故下设计模式,嘿嘿。
Nio中利用面向对象的多态性,使用多重继承将Buffer模块设计的鬼斧神工。

对每种数据类型的Buffer都定义其抽象的数据类型Buffer,如:
ByteBuffer/ShortBuffer/IntBuffer等抽象类,然后针对堆Buffer和直接内存Buffer两种形态又设计各自的实现:HeapByteBuffer/DirectByteBuffer、HeapShortBuffer/DirectShortBuffer、HeapIntBuffer/DirectIntBuffer。

总结

至此,Buffer的类容介绍完毕。这里只是浅谈了Buffer的原理、行为和分类,并没有对源码进行解读,感兴趣的朋友可以自行阅读。

这里再做下简单的总结,各种类型的Buffer主要是利用线性数组存储各种类型的数据,维护多个属性位,伴随将属性位变化达到读写状态的变化。读写操作的多态形式使得Buffer的api机器丰富,可用性极强。Buffer的种类可谓纷繁,每种Buffer都有其各自的应用场景,解决不同的需求。

参考

java NIO详解
NIO学习–缓冲区
Java 语言中一个字符占几个字节

以上是关于Buffer的主要内容,如果未能解决你的问题,请参考以下文章

具有多个缓冲区的片段着色器颜色错误

HLS.js 获取视频片段信息

20179209《Linux内核原理与分析》第十二周作

带有片段着色器的OpenGL 3.3不同颜色

WebGL中Stencil Buffer的运用以及ThreeJS的实现

从片段着色器中的地形高程数据计算法线