type
status
date
urlname
summary
tags
category
icon
password
catalog
sort
在上一章中我们介绍的 NIO模型的核心组件 以及其运行流程
知道如何简单使用 buffer 之后,下面就来探究一下 buffer 的底层原理。
Buffer 的原理主要在于它的四个属性
  • mark:在缓冲区操作过程当中,可以将当前的 position 的值临时存入 mark 属性中,需要的时候再取出,恢复到之前的 position,重新从之前的 position 位置开始处理。调用 mark() 方法来设置 mark=position,再调用 reset() 可以让 position 恢复到 mark 标记的位置,即 position=mark
  • position:位置,读或写都会改变位置
  • limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,极限可以修改
  • capacity:容量,在缓冲区创建时设定且不可修改,比如前面创建的缓冲区容量就是 1024 且不可修改
mark 属性和 capacity 已经介绍的很详细了,mark 属性一般情况下不需要用它,而 capacity 就是容量在创建 buffer 时就已经固定了,因此下面重点介绍其它属性。
一个刚创建的 buffer 是写模式,比如 ByteBuffer.allocate(10),此时 position=0,limit=10,如下图所示。
notion image
limit 表示读或写的极限,由于当前是写模式,因此可以写 10 个数据,且每写入一次数据,position 就会加 1。如果写第 11 个数据,则会报错。
limit 是可以修改的,比如改成 5,则最多只能放 5 个数据,写入第 6 个时就会报错,如下图所示。
notion image
当调用 flip 方法时,会从写模式切换到读模式。我们看看 flip 方法做了什么。
我们假设一下,新创建的 buffer 容量为 10,且设置了 limit=8,那么当写了 5 个数据后(未写满)就调用一下 flip 方法,调用之前 limit=8,position=5,调用之后 limit=5,而 position 重置为 0,此时已经进入读模式,且会读取 0~5 之间的数据,因为你只写了 5 个啊。
以上的分析就是 position 和 limit 属性的含义,它会控制读或写的区间永远在 position 到 limit 的之间。
如何从写模式切换到读模式我们已经知道了,那么如何从读模式切换到写模式呢?答案是调用 clear 方法或 compact 方法。下面看一下 clear 方法做了什么。
其这么做的含义你们可以自行研究,前面道理已经讲得比较清楚了。
下面是读写模式切换方式图。
notion image
 

Buffer 核心原理

Capacity

Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity容量,缓冲区就满了,不能再写入了。
Buffer类的capacity属性一旦初始化,就不能再改变。原因是什么呢?Buffer类的对象在初始化时,会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小当然就不能改变了。
前面讲到,Buffer类是一个抽象类,Java不能直接用来新建对象。在具体使用的时候,必须使用Buffer的某个子类,例如DoubleBuffer子类,该子类能写入的数据类型是double类型,如果在创建实例时其capacity是100,那么我们最多可以写入100个double类型的数据。
capacity容量并不是指内部的内存块byte[]数组的字节数量,而是指能写入的数据对象的最大限制数量。
 
 

Position

Buffer类的position属性,表示当前的位置。position属性的值与缓冲区的读写模式有关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。
在写入模式下,position的值变化规则如下:
  1. 在刚进入到写入模式时,position值为0,表示当前的写入位置为从头开始。
  1. 每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
  1. 初始的position值为0,最大可写值为limit–1。当position值达到limit时,缓冲区就已经无空间可写了。
在读模式下,position的值变化规则如下:
  1. 当缓冲区刚开始进入到读取模式时,position会被重置为0。
  1. 当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
  1. 在读模式下,limit表示可以读上限。position的最大值,为最大可读上限limit,当position达到limit时,表明缓冲区已经无数据可读。
Buffer的读写模式具体如何切换呢?当新建了一个缓冲区实例时,缓冲区处于写入模式,这时是可以写数据的。在数据写入完成后,如果要从缓冲区读取数据,这就要进行模式的切换,可以使用(即调用)flip翻转方法,将缓冲区变成读取模式。
在从写入模式到读取模式的flip翻转过程中,position和limit属性值会进行调整,具体的规则是:
  1. limit属性被设置成写入模式时的position值,表示可以读取的最大数据位置;
  1. position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读。
 
 

Limit

Buffer类的limit属性,表示可以写入或者读取的最大上限,其属性值的具体含义,也与缓冲区的读写模式有关,在不同的模式下,limit的值的含义是不同的,具体分为以下两种情况:
  1. 在写入模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入到写入模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。
  1. 在读取模式下,limit的值含义为最多能从缓冲区中读取到多少数据。
一般来说,在进行缓冲区操作时,是先写入然后再读取的。当缓冲区写入完成后,就可以开始从Buffer读取数据,可以使用flip翻转方法,这时,limit的值也会进行调整。具体如何调整呢?将写入模式下的position值,设置成读取模式下的limit值,也就是说,将之前写入的最大数量,作为可以读取的上限值。
Buffer在flip翻转时的属性值调整,主要涉及position、limit两个属性,但是这种调整比较微妙,不是太好理解,下面是一个简单例子:
首先,创建缓冲区。新创建的缓冲区处于写入模式,其position值为0,limit值为最大容量capacity。
然后,向缓冲区写数据。
每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。
最后,使用flip方法将缓冲区切换到读模式。limit的值,先会被设置成写入模式时的position值,所以新的limit值是5,表示可以读取的最大上限是5。之后调整position值,新的position会被重置为0,表示可以从0开始读。
缓冲区切换到读模式后,就可以从缓冲区读取数据了,一直到缓冲区的数据读取完毕。
除了以上capacity(容量)、position(读写位置)、limit(读写的限制)三个重要属性之外,Buffer还有一个比较重要的标记属性:mark(标记)属性。该属性的大致作用为:在缓冲区操作过程当中,可以将当前的position的值临时存入mark属性中;需要的时候,可以再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理。
 

Buffer类的重要方法

本小节将详细介绍Buffer类常用的几个方法,包含Buffer实例创建、对Buffer实例的写入、读取、重复读、标记和重置等。

allocate()创建缓冲区

在使用Buffer(缓冲区)实例之前,我们首先需要获取Buffer子类的实例对象,并且分配内存空间。如果需要获取一个Buffer实例对象,并不是使用子类的构造器来创建一个实例对象,而是调用子类的allocate()方法。
下面的程序片段,演示如何获取一个整型的Buffer实例对象,代码如下:
例中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(20),创建了一个Intbuffer实例对象,并且分配了20*4个字节的内存空间。运行程序之后,通过程序的输出结果,我们可以查看一个新建缓冲区实例对象的主要属性值,如下所示:
从上面的运行结果,可以看出:一个缓冲区在新建后,处于写入的模式,position属性(代表写入位置)的值为0,缓冲区的capacity容量值也是初始化时allocate方法的参数值(这里是20),而limit最大可写上限值也为的allocate方法的初始化参数值。

put()写入到缓冲区

在调用allocate方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,而如果要写入对象到缓冲区,需要调用put方法。put方法很简单,只有一个参数,即为所需要写入的对象。只不过,写入的数据类型要求与缓冲区的类型保持一致。
接着前面的例子,向刚刚创建的intBuffer缓存实例对象中,写入的5个整数,代码如下:
写入5个元素后,同样输出缓冲区的主要属性值,输出的结果如下:
从结果可以看到,写入了5个元素之后,缓冲区的position属性值变成了5,所以指向了第6个(从0开始的)可以进行写入的元素位置。而limit最大可写上限、capacity最大容量两个属性的值,都没有发生变化。

flip()翻转

向缓冲区写入数据之后,是否可以直接从缓冲区中读取数据呢?呵呵,不能。为什么呢?这时缓冲区还处于写模式,如果需要读取数据,还需要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读取模式。
接着前面的例子,演示一下flip()方法的使用:
在调用flip方法进行缓冲区的模式翻转之后,通过程序的输出内容可以看到,缓冲区的属性有了奇妙的变化,具体如下:
调用flip方法后,新模式下可读上限limit的值,变成了之前写入模式下的position属性值,也就是5;而新的读取模式下的position值,简单粗暴地变成了0,表示从头开始读取。
对flip()方法的从写入到读取转换的规则,再一次详细的介绍如下:
  1. 首先,设置可读上限limit的属性值。将写入模式下的缓冲区中内容的最后写入位置position值,作为读取模式下的limit上限值。
  1. 其次,把读的起始位置position的值设为0,表示从头开始读。
  1. 最后,清除之前的mark标记,因为mark保存的是写入模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记,会造成位置混乱。
有关上面的三步,其实可以查看Buffer.flip()方法的源代码,具体代码如下:
当然,新的问题来了:在读取完成后,如何再一次将缓冲区切换成写入模式呢?答案是:可以调用Buffer.clear()
清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为写模式。总体的Buffer模式转换,大致如图缓冲区读写模式的转换所示。

get()从缓冲区读取

使用调用flip方法将缓冲区切换成读取模式之后,就可以开始从缓冲区中进行数据读取了。读取数据的方法很简单,可以调用get方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
接着前面flip的使用实例,演示一下缓冲区的读取操作,代码如下:
以上代码调用get方法从缓冲实例中先读取2个,再读取3个元素,运行后,输出的结果如下:
从程序的输出结果,我们可以看到,读取操作会改变可读位置position的属性值,而limit可读上限值并不会改变。在position值和limit的值相等时,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了。此时再读,会抛出BufferUnderflowException异常。
那么,在读完之后是否可以立即对缓冲区进行数据写入呢?答案是不能。现在还处于读取模式,我们必须调用Buffer.clear()或Buffer.compact()方法,即清空或者压缩缓冲区,将缓冲区切换成写入模式,让其重新可写。
此外还有一个问题:缓冲区是不是可以重复读呢?答案是可以的,既可以通过倒带方法rewind()去完成,也可以通过mark(
)和reset( )两个方法组合实现。

rewind()倒带

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。
接着前面的示例代码,继续rewind方法使用的演示,示例代码如下:
这个范例程序的执行结果如下:
rewind()方法,主要是调整了缓冲区的position属性与mark标记属性,具体的调整规则如下:
  1. position重置为0,所以可以重读缓冲区中的所有数据;
  1. limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量;
  1. mark标记被清理,表示之前的临时位置不能再用了。
从JDK中可以查阅到Buffer.rewind()方法的源代码,具体如下:
通过源代码,我们可以看到rewind()方法与flip()很相似,区别在于:倒带方法rewind()不会影响limit属性值;而翻转方法flip()会重设limit属性值。
在rewind倒带之后,就可以再一次读取,重复读取的示例代码如下:
这段代码,和前面的读取示例代码基本相同,只是增加了一个mark调用。大家可以通过随书源码工程执行以上代码并观察输出结果,具体的输出与前面的类似,这里不做赘述。

mark()和reset()

mark()和reset()两个方法是成套使用的:Buffer.mark()方法将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark的值恢复到position中。
说明
Buffer.mark()和Buffer.reset()两个方法都涉及到mark属性的使用。mark()方法与mark属性,二者的名字虽然相同,但是一个是Buffer类的成员方法,另一个是Buffer类的成员属性,不能混淆。
例如,可以在前面重复读取的示例代码中,在读到第3个元素(i为2时)时,可以调用mark()方法,把当前位置position的值保存到mark属性中,这时mark属性的值为2。
然后,就可以调用reset()方法,将mark属性的值恢复到position中,这样就可以从位置2(第三个元素)开始重复读取。
继续接着前面重复读取的代码,进行mark()方法和reset()方法的示例演示,代码如下:
在上面的代码中,首先调用reset()把mark中的值恢复到position中,因此读取的位置position就是2,表示可以再次开始从第3个元素开始读取数据。上面的程序代码的输出结果是:
调用reset方法之后,position的值为2,此时去读取缓冲区,输出了后面的三个元素为2、3、4。

clear()清空缓冲区

在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法的作用:
  1. 会将position清零;
  1. limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。
接着上面的实例,演示一下clear( )方法的使用,大致的代码如下:
这个程序运行之后,结果如下:
在缓冲区处于读取模式时,调用clear(),缓冲区会被切换成写入模式。调用clear()之后,我们可以看到清空了position(写入的起始位置)的值,其值被设置为0,并且limit值(写入的上限)为最大容量。
 
总体来说,使用Java NIO Buffer类的基本步骤如下:
(1)使用创建子类实例对象的allocate( )方法,创建一个Buffer类的实例对象。
(2)调用put( )方法,将数据写入到缓冲区中。
(3)写入完成后,在开始读取数据前,调用Buffer.flip( )方法,将缓冲区转换为读模式。
(4)调用get( )方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写入模式,可以继续写入
Java IO — NIO ChannelJava IO — IO/NIO模型
Loading...
Honesty
Honesty
人道洛阳花似锦,偏我来时不逢春
最新发布
Java IO — NIO Buffer
2024-10-21
Java IO — NIO Channel
2024-10-21
Java IO — IO/NIO模型
2024-10-21
Java异步编程方式介绍
2024-10-21
Elasticsearch — 索引(Mapping Index)
2024-10-19
Elasticsearch — 如何存储数据并保持一致性?
2024-10-19