JavaIO(三)-NIO缓冲区详解这一篇就够了

BIO一个线程对应一个连接和其阻塞的本质,已经无法适应当下的互联网环境。那NIO为什么就能?

NIO三件套:缓冲区(Buffer),通道(Channel),选择器(Selector)。缓冲区是工具,通道是桥梁,选择器是核心。


缓冲区(Buffer):

之前我们说过,所谓“I/O”讲的无非就是把数据移进或移出缓冲区。而NIO为我们提供了专门针对各种缓冲区的专业API。所以我开头才说,Buffer只是工具,重点是会用。

所谓Buffer可以暂时理解为一个数组对象,具有数组的属性定长、类型一致、索引。一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。

 

各类型缓冲区一定具备以下几种属性,这也是所有API的基石

 

容量(Capacity):和数组大小一样,是缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。

上界(Limit):缓冲区的第一个不能被读或写的元素下标索引,在缓冲区被创建时初始值等同于Capacity。

位置(Position):下一个要被读或写的元素的索引,数值会随着数据的读取和写入发生变动,在没有数据写入的时候,初始值为0。

标记(Mark):一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。初始值-1,但是Buffer没有负值,所以是未定义的(undefined)。

这四个属性之间总是遵循以下关系:

mark <= position <= limit <= capacity

所以一个新建的容量为10的Buffer

 

位置被设为 0,而且容量和上界被设为 10,刚好经过缓冲区能够容纳的最后一个字节。标记最初未定义。容量是固定的,但另外的三个属性可以在使用缓冲区时改变。这三个属性都可以通过以下API操作

 

buffer.capacity();//获取容量
buffer.position();//获取位置
buffer.limit();//获取上界
buffer.position(2);//设置位置
buffer.limit(5);//设置上界
buffer.mark();//设置标记

缓冲区API

现在以字节缓冲区ByteBuffer为例介绍其中的一部分API的使用

1.缓冲区的创建,创建一个容量为10的缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(10);
byte[] myArray = new byte[10];
ByteBuffer byteBuffer2 = ByteBuffer.wrap(myArray);
ByteBuffer byteBuffer3 = ByteBuffer.wrap(myArray, 1, 5);

ByteBuffer为我们提供了4种创建方式

①第一种,在堆中初始化一个容量为10的缓冲区,所有的属性都是初始值,跟上边的图一样。

②第二种,创建一个直接缓冲区,这个缓冲区,不在堆中。

③第三种,基于一个byte数组创建一个字节缓冲区,不同类型的数组只能创建同类型的缓冲区,capacity等于数组大小,其余所有属性属于初始化状态,但是数组中如果有数据,会被同步拷贝到新创建的ByteBuffer 中,可以理解为ByteBuffer 是这个数组的"包装类",我们对这个缓冲区所有的操作都会影响数组,相应的所有的数组相关操作,都会影响缓冲区。

④第四种:在第三种的基础上,新增了2个参数,分别用于设置偏移量和需要使用的长度,这两个参数会直接影响ByteBuffer 中的position和limit属性,position=偏移量, 因为需要设置上界,所以limit=偏移量+使用长度<=capacity=数组长度,如果越界 ,就会报IndexOutOfBoundsException,如上边代码中,在创建完成之后position为1,limit为6<=10。

上面四种,①③④都是创建的间接缓冲区,与第二种直接缓冲区存在本质上的不同。

间接缓冲区是被分配到JVM堆上的,如果是间接缓冲区操作通常会经历以下操作:

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

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

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

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

也就是说,即使是创建非直接缓冲区,还是需要创建直接缓冲区的。直接缓冲区使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈。建立和销毁直接缓冲区当然也会比在堆栈中的缓冲区更加复杂,这取决于主操作系统以及 JVM 实现。

直接 ByteBuffer 是通过调用具有所需容量的 ByteBuffer.allocateDirect()函数产生的,所有的缓冲区都提供了一个叫做 isDirect()的 boolean 函数,来测试特定缓冲区是否为直接缓冲区。

2.缓冲区的读写,get()或 put()函数,每一个 Buffer 类都有。get和 put 可以是相对的也可以是绝对的。

最简单的put()和get()都会使position前进1,put可以通过级联的方式调用进行连续使用。

CharBuffer charBuffer = CharBuffer.allocate(10);
charBuffer.put('H').put('e').put('l').put('l').put('o');
System.out.println(charBuffer.position());
charBuffer.flip();//翻转 后讲
System.out.println(charBuffer.position());
System.out.println(charBuffer.get());
System.out.println(charBuffer.position());

输出结果

 

第一行创建一个容量为10的缓冲区

 

第二行连续输入成员Hello

 

第四行翻转

 

第六行get

 

虽然flip()还没有说,但是上边的整个流程大概就是一个缓冲区的基本操作。然后put和get还有两个带参数的方法,也就是我们所说的绝对。

CharBuffer charBuffer = CharBuffer.allocate(10);
charBuffer.put('H').put('e').put('l').put('l').put('0');
charBuffer.put(8, 'I');
System.out.println(charBuffer.position());
System.out.println(charBuffer.get(8));
System.out.println(charBuffer.position());

 

绝对操作只要不越界,不会对四大基本属性造成影响,该操作相当于只是对对应数组下标索引位置的值进行赋值和获取,这个索引同样必须小于limit。

记住在 java 中,字符在内部以 Unicode 码表示,每个 Unicode 字符占 16 位。如果我用的是ByteBuffer而不是CharBuffer,那么就必须将char 强制转换为 byte,我们删除了前八位来建立一个八位字节值。所以什么类型的Buffer只能接收什么类型的数据。

3.flip(),缓冲区翻转、切换

如果我们已经写满了缓冲区,现在我们必须准备将其清空。但如果通道现在在缓冲区上执行 get(),那么它将从position开始读取,然后将position+1,还是以上边的Hello为例,我们调用get()方法,读取下标索引为5的位置的数据只能读取到空字符,并且position变为6。如果我们可以将位置值重新设为 0,那就可以从一开始读取我们的数据了,这就是我们的flip的作用。

public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。观察我们的flip的源码,可能再清晰明了不过了,flip操作会将我们的position设置为0,不管原来是多少都是设置成0,然后将我们的limit = position,规定了此次读取的上边界。

4.clear()和compact(),缓冲区清理

缓冲区有写到读的切换,那么一定就有读到写的切换。如果我们已经写满一个缓冲区,并且我们进行了切换然后把所有数据都读完了,那么此时缓冲区对我们还有用,可以重复利用嘛,不想销毁,那么这时候我们可以调用clear方法将缓冲区恢复到初始形态。

public Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

查看clear()源码,我们发现,数据时没有清除的,只不过将所有的属性,恢复到初始化状态,因为缓冲区大小、类型在我们创建时就已经确定,所以分配的空间大小已经确定,那么里边的数据清不清其实没关系。

compact()就比较复杂了,他是真的把数据删了,严格来说不是把数据删了,而是压缩了。怎么理解这个压缩?

CharBuffer buffer = CharBuffer.allocate(10);
buffer.put('H').put('e').put('l').put('l').put('o');
buffer.flip();
System.out.println(buffer.get());
buffer.compact();

以上边代码为例,此时'H'已经被我读取了,那么如果我认为,'H'已经对我没用了,那么'H'就有可能成为我后续所有操作的一个影响,增加我的复杂度,所以我要把这个'H'删掉。以上代码截止到第4行,这个缓冲区各参数的位置是这样的

 

当我们调用compact()方法之后

 

'H'已经没有了,compact()先将1-4位置的数据copy到0-3,并将position放到了4(limit-position)的位置上,并使limit回归到初始状态capacity,现在当我们开始写入数据,原先4位置的数据也会被我们直接顶替掉,这样才达成了压缩的目的。通过一系列操作之后减少了数据的复杂性。

5.mark(),不重要的知识增加了

mark我们最一开始就已经说过了,标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在 mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调用 reset( )将导致 InvalidMarkException 异常。

也就是说 mark( )函数使 mark=position, reset( )函数使position=mark,很少使用。

6.比较

前面的类图我们注意到,所有的缓冲区都是实现了Comparable接口的,所以都具有比较属性,那么这里的比较和equals有什么区别。所有的缓冲区都提供了一个常规的equals( )函数用以测试两个缓冲区的是否相等,以及一个 compareTo( )函数用以比较缓冲区。如果每个缓冲区中剩余的内容相同,那么 equals( )函数将返回 true,否则返回 false。

  • 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
  • 两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相

同。

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

如果不满足以上任意条件,就会返回 false。下边这两个就是相等的,为true

 

CharBuffer buffer = CharBuffer.allocate(10);
buffer.put('e').put('l').put('l').put('o').put('o');
buffer.position(2);
buffer.limit(4);
CharBuffer buffer1 = CharBuffer.allocate(5);
buffer1.put('H').put('e').put('l').put('l').put('o');
buffer1.position(3);
System.out.println(buffer.equals(buffer1));

缓冲区也支持用 compareTo( )函数以词典顺序进行比较。比较是针对每个缓冲区内剩余数据进行的,与它们在 equals( )中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值