缓冲区不能为空。 参数名: buffer_Java NIO2:缓冲区

什么是缓冲区

一个缓冲区对象是固定数量的数据的容器,其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。缓冲区像前篇文章讨论的那样被写满和释放,对于每个非布尔原始数据类型都有一个缓冲区类,尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节,非字节缓冲区可以再后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。

缓冲区的工作与通道紧密联系。通道是I/O传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,待传递出去的数据被置于一个缓冲区,被传送到通道;待传回的缓冲区的传输,一个通道将数据放置在所提供的缓冲区中。这种在协同对象之间进行的缓冲区数据传递时高效数据处理的关键。

Buffer类的家谱

下图是Buffer的类层次图。在顶部是通用Buffer类,Buffer定义所有缓冲区类型共有的操作,无论是它们所包含的数据类型还是可能具有的特定行为:

0f8b524616f4b6a15348f49ca9fcd782.png

缓冲区基础

概念上,缓冲区是包在一个对象内的基本数据元素数组。Buffer类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中,Buffer类以及它专有的子类定义了一个用于处理数据缓冲区的API。下面来看一下Buffer类所具有的属性和方法:

1、属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息,它们是:

2573773f70fec7a5308b45aa97a77e5a.png

这四个属性总是遵循以下的关系:0 <= mark <= position <= limit <= capacity

2、方法

下面看一下如何使用一个缓冲区,Buffer中提供了以下的一些方法:

3f8f7a8933058e9c6c8181efa5ee4eec.png

关于这个API有一点值得注意的,像clear()这类函数,通常应当返回的是void而不是Buffer引用。这些函数将引用返回到它们在(this)上被引用的对象,这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:

buffer.mark();buffer.position(5);buffer.reset();

被简写成:

buffer.mark().position(5).reset();

缓冲区代码实例

对缓冲区的使用,先看一段代码,然后解释一下:

450ccf1afdb23e54480cfa013f6415a6.png

逐一解释一下:

1、第52行,CharBuffer是一个抽象类,它不能被实例化,因此利用allocate方法来实例化,相当于是一个工厂方法。实例化出来的是HeapCharBuffer,默认大小是100。根据上面的Buffer的类家族图谱,可以看到每个Buffer的子类都是使用allocate方法来实例化具体的子类的,且实例化出来的都是Heap*Buffer

2、第24行~第36行,每次取String数组中的一个,利用put方法放置一个数据进入CharBuffer中

3、第55行,调用flip方法,这是非常重要的。在缓冲区被写满后,必须将其清空,但是如果现在在通道上直接执行get()方法,那么它将从我们刚刚插入的有用数据之外取出未定义数据;如果此时将位置重新设置为0,就会从正确的位置开始获取数据,但是如何知道何时到达我们所插入数据末端呢?这就是上界属性被引入的目的----上界属性指明了缓冲区有效内容的末端。因此,在读取数据的时候我们需要做两件事情:

(1)将上界属性limit设置为当前位置 (2)将位置position设置为0

这两步操作,JDK API给开发者提供了一个filp()方法来完成,flip()方法将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态,因此每次准备读出元素前,都必须调用一次filp()方法

4、第41行~第48行,每次先判断一下是否已经达到缓冲区的上界,若存在则调用get()方法获取到此元素,get()方法会自动移动下标position

5、第57行,对Buffer的操作完成之后,调用clear()方法将所有属性回归原位,但是clear()方法并不会改变缓冲区中的任何数据

缓冲区比较

缓冲区的比较即equals方法,缓冲区的比较并不像我们想像得这么简单,两个缓冲区里面的元素一样就是相等,两个缓冲区相等必须满足以下三个条件:

1、两个对象类型相同,包含不同数据类型的buffer永远不会像等,而且buffer绝不会等于非buffer对象

2、两个对象都剩余相同数量的元素,Buffer的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从position到limit)必须相同

3、在每个缓冲区中应被get()函数返回的剩余数据元素序列必须一致

如果不满足上面三个条件,则返回false。下面两幅图演示了两个缓冲区相等和不相等的场景,首先是两个属性不同的缓冲区也可以相等:

eb86aff8b4899acf2f5f475e51122049.png

然后是两个属性相同但是被等为不相等的缓冲区:

569478581bf4f88ca2281650e7163fe5.png

批量移动数据

缓冲区的设计目的就是为了能够高效地传输数据。一次移动一个数据元素,其实并不高效,如在下面的程序清单中所看到的那样,Buffer API提供了向缓冲区内外批量移动数据元素的函数:

2dc98321d8620abcebe0abe51c37211b.png

其实这种批量移动的合成效果和前文的循环在底层实现上是一样的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。

字节缓冲区

字节缓冲区和其他缓冲区类型最明显的不同在于,它们可能成为通道所执行I/O的源头或目标,如果对NIO有了解的朋友们一定知道,通道只接收ByteBuffer作为参数。

如我们所知道的,操作系统在内存区域进行I/O操作,这些内存区域,就操作系统方面而言,是相连的字节序列。于是,毫无疑问,只有字节缓冲区有资格参与I/O操作。也请回想一下操作系统会直接存取进程----在本例中是JVM进程的内存空间,以传输数据。这也意味着I/O操作的目标内存区域必须是连续的字节序列,在JVM中,字节数组可能不会在内存中连续存储,或者无用存储单元收集可能随时对其进行移动。在Java中,数组是对象,而数据存储在对象中的方式在不同的JVM实现中各有不同。

出于这一原因,引入了直接缓冲区的概念。直接缓冲区被用于与通道和固有I/O线程交互,它们通过使用固有代码来告知操作系统直接释放或填充内存区域,对用于通道直接或原始存取的内存区域中的字节元素的存储尽了最大的努力。

直接字节缓冲区通常是I/O操作最好的选择。在设计方面,它们支持JVM可用的最高效I/O机制,非直接字节缓冲区可以被传递给通道,但是这样可能导致性能损耗,通常非直接缓冲不可能成为一个本地I/O操作的目标,如果开发者向一个通道中传递一个非直接ByteBuffer对象用于写入,通道可能会在每次调用中隐含地进行下面的操作:

1、创建一个临时的直接ByteBuffer对象

2、将非直接缓冲区的内容复制到临时缓冲中

3、使用临时缓冲区执行低层次I/O操作

4、临时缓冲区对象离开作用于,并最终成为被回收的无用数据

这可能导致缓冲区在每个I/O上复制并产生大量对象,而这种事都是我们极力避免的。

直接缓冲区是I/O的最佳选择,但可能比创建非直接缓冲区要花费更高的成本。直接缓冲区使用的内存是通过调用本地操作系统的代码分配的,绕过了标准JVM堆栈。建立和销毁直接缓冲区会明显比具有堆栈的缓冲区更极爱破费,这取决于主操作系统以及JVM实现。直接缓冲区的内存区域不受无用存储单元收集支配,因为它们位于标准JVM堆栈之外。

直接ByteBuffer是通过调用具有所需容量的ByteBuffer.allocateDirect()函数产生的:

27bd2105b66fe10749445298f95eef75.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值