清华教授!手抄NIO Buffer类与属性万字详解,一文解决高并发

50 篇文章 0 订阅

上一篇分享的是《Redis 五种数据结构以及三种高级数据结构解析》,这篇给大家分享《详解NIO Buffer类》。

详解NIO Buffer类

NIO的Buffer本质上是一个内存块,既可以写入数据,也可以从中读取数据。Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。

NIO的Buffer内部是一个内存块(数组),与普通的内存块(Java数组)不同的是:NIO Buffer对象提供了一组比较有效的方法,用来进行写入和读取的交替访问。

tips: Buffer类是一个非线程安全类。(Buffers are not safe for use by multiple concurrent threads.)

1 Buffer类API介绍

Buffer类是一个抽象类,对应于Java的主要数据类型。在NIO中,有8种缓冲区类,ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。前7种Buffer类型覆盖了能在IO中传输的所有Java基本数据类型,第8种类型是一种专门用于内存映射的ByteBuffer类型。

使用最多的是ByteBuffer(二进制字节缓冲区)类型

1.1 Buffer类的重要属性

Buffer的子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer基类中,而是定义在具体的子类中。例如,ByteBuffer子类就拥有一个byte[]类型的数组成员final byte[] hb,可以作为自己的读写缓冲区,数组的元素类型与Buffer子类的操作类型相对应。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
    
    final byte[] hb; 

}

为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)

1.1.1 capacity 属性,position属性,limit属性

  1. capacity 属性

Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。

Buffer类的capacity属性一旦初始化,就不能再改变。

Buffer类的对象在初始化时会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小就不能改变了。

  1. position属性

Buffer类的position属性表示当前的位置。

position属性的值与缓冲区的读写模式有关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。

在写模式下,position值的变化规则如下:

(1)在刚进入写模式时,position值为0,表示当前的写入位置为从头开始。

(2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。

(3)初始的position值为0,最大可写值为limit-1。当position值达到limit时,缓冲区就已经无空间可写了。

在读模式下,position值的变化规则如下:

(1)当缓冲区刚开始进入读模式时,position会被重置为0。

(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。

(3)在读模式下,limit表示可读数据的上限。position的最大值为最大可读上限limit,当position达到limit时表明缓冲区已经无数据可读。

当新建了一个缓冲区实例时,缓冲区处于写模式,这时是可以写数据的。在数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式,flip为翻转的意思。

在从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:

(1)limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。

(2)position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。

也就是说flip把limit设为position,再把position设为0。

  1. limit属性

Buffer类的limit属性表示可以写入或者读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关。

在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。

在读模式下,limit值的含义为最多能从缓冲区读取多少数据。

一般来说,在进行缓冲区操作时是先写入再读取的。当缓冲区写入完成后,就可以开始从Buffer读取数据,调用flip()方法(翻转),这时limit的值也会进行调整。将写模式下的position值设置成读模式下的limit值,也就是说,将之前写入的最大数量作为可以读取数据的上限值。

Buffer在翻转时的属性值调整主要涉及position、limit两个属性,下面举一个简单的例子:

(1) 首先,创建缓冲区。新创建的缓冲区处于写模式,其position值为0,limit值为最大容量capacity。

(2)然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。

(3)最后,使用flip方法将缓冲区切换到读模式。limit的值会先被设置成写模式时的position值,所以新的limit值是5,表示可以读取数据的最大上限是5。之后调整position值,新的position会被重置为0,表示可以从0开始读。

标记属性:mark(标记)属性

在缓冲区操作过程当中,可以将当前的position值临时存入mark属性中;需要的时候,再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理。

这里主要涉及两个方法:

  • mark方法:将当前的position值临时存入mark属性中
  • reset方法:恢复到position属性中

1.2 Buffer类中的重要方法

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

1.2.1 allocate():创建Buffer实例

@Test
public void testAllocate(){
    // 参数:指定capacity属性,不能小于0
    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
    System.out.println("position = " + byteBuffer.position());
    System.out.println("capacity = " + byteBuffer.capacity());
    System.out.println("limit = " + byteBuffer.limit());
}

输出结果:

position = 0
capacity = 100
limit = 100

一个缓冲区在新建后处于写模式,position属性(代表写入位置)的值为0,缓冲区的capacity值是初始化时allocate方法的参数值(这里是100),而limit最大可写上限值也为allocate方法的初始化参数值。

1.2.2 put方法:写入数据

在调用allocate()方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,

如果要把对象写入缓冲区,就需要调用put()方法。put()方法很简单,只有一个参数,即需要写入的对象,只不过要求写入的数据类型与缓冲区的类型保持一致。

@Test(expected = BufferOverflowException.class)
public void testPut(){
    IntBuffer buffer = IntBuffer.allocate(5);
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    System.out.println("position = " + buffer.position());
    System.out.println("capacity = " + buffer.capacity());
    System.out.println("limit = " + buffer.limit());
    // 转成数数组
    int[] array = buffer.array();
    Arrays.stream(array).forEach(System.out::print);
    // 前面已经写入5个数据了,尝试在写入就会抛出异常: BufferOverflowException
    buffer.put(6);
}

输出:

position = 5
capacity = 5
limit = 5
12345

1.2.3 flip()

向缓冲区写入数据之后,是否可以直接从缓冲区读取数据呢?不能!这时缓冲区还处于写模式,如果需要读取数据,要将缓冲区转换成读模式。

@Test
public void testFlip() {
    IntBuffer buffer = IntBuffer.allocate(20);
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    System.out.println("调用flip之前");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    // 调用
    buffer.flip();
    System.out.println("调用flip之后");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}

输出:

调用flip之前
position = 5 ;capacity = 20 ;limit = 20
调用flip之后
position = 0 ;capacity = 20 ;limit = 5

调用flip()方法后,新模式下可读上限limit的值变成了之前写模式下的position属性值,也就是5;而新的读模式下的position值简单粗暴地变成了0,表示从头开始读取。

对flip()方法从写入到读取转换的规则,:

  1. 设置可读上限limit的属性值。将写模式下的缓冲区中内容的最后写入位置position值作为读模式下的limit上限值。
  2. 其次,把读的起始位置position的值设为0,表示从头开始读。
  3. 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记,就会造成位置混乱。

源码也很简单就是干了这三件事

public final Buffer flip() {
    // 设置可读上限limit,设置为写模式下的position值
    limit = position;
    // 把读的起始位置position的值设为0,表示从头开始读
    position = 0;
    // 清除之前的mark标记
    mark = -1;
    // 返回当前实例
    return this;
}

1.2.4 get方法:读取数据

get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。

@Test
public void testGet1() {
    IntBuffer buffer = IntBuffer.allocate(20);
    System.out.println("刚初始化buffer:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    System.out.println("调用flip之前:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.flip();
    System.out.println("调用flip之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    int i = buffer.get();
    System.out.println("调用get之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}

输出:

刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后:
position = 1 ;capacity = 20 ;limit = 5

分析:

  1. 声明的buffer的capacity是20个数据,此时position = 0 ;capacity = 20
  2. 放了5个数据之后,position就从零一定移动到了5
  3. 调用flip之后,准备读取数据,就会把position置为0,limit置为刚刚的5(因为现在只有5个数据可读)
  4. 调用get读取数据(读取出当前position位置的数据,此时就是0位置出的数据那就是1),position就从0移动了1个位置,position = 1了

读取操作会改变可读位置position的属性值,而可读上限limit值并不会改变。在position值和limit值相等时,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了,此时再读就会抛出BufferUnderflowException异常。

get还有一个重载方法,可以读取指定索引的数据,但是这个方法不会移动position

@Test
public void testGet2() {
    IntBuffer buffer = IntBuffer.allocate(20);
    System.out.println("刚初始化buffer:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    System.out.println("调用flip之前:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.flip();
    System.out.println("调用flip之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    int i = buffer.get(2);
    System.out.println("调用get之后,读取的数据是:" + i);
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}

输出

刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后,读取的数据是:3
position = 0 ;capacity = 20 ;limit = 5

此时position没有移动,读取出的数据就是的position为2的数据也就是3

1.2.5 clear()清空 / compact()压缩方法

flip是将缓冲区转换成读模式,而这两个它们可以将缓冲区转换为写模式。

  1. clear: 把极限设为容量,再把位置设为0。
  2. compact()方法:删除缓冲区内从0到当前位置position的内容,然后把从当前位置position到极限limit的内容拷贝到0到limit-position的区域内,当前位置position和极限limit的取值也做相应的变化

compact: 就是把已经读取出来的空的部分删除,把之前未读取的数据推到buffer的开始位置

 

@Test
public void testCompact() {
    IntBuffer buffer = IntBuffer.allocate(20);
    System.out.println("刚初始化buffer:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    System.out.println("调用flip之前:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.flip();
    System.out.println("调用flip之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    int i = buffer.get();
    System.out.println("调用get之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.compact();
    System.out.println("调用compact之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    buffer.put(100);
    System.out.println("再次写入数据:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}
刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后:
position = 1 ;capacity = 20 ;limit = 5
调用compact之后:
position = 4 ;capacity = 20 ;limit = 20
再次写入数据:
position = 5 ;capacity = 20 ;limit = 20

分析:

  1. 声明的buffer的capacity是20个数据,此时position = 0 ;capacity = 20
  2. 放了5个数据之后,position就从零一定移动到了5
  3. 调用flip之后,准备读取数据,就会把position置为0,limit置为刚刚的5(因为现在只有5个数据可读)
  4. 调用get读取数据,position就从0移动了1个位置,position = 1了
  5. 调用compact之后,删除缓冲区内从0到当前位置position的内容,然后把从当前位置position到极限limit的内容拷贝到0到limit-position的区域内(就会压缩掉刚刚已经读取的出了那个数据的位置),然后把limit设置为capacity,这样就可以继续写入数据(这里就会从position=4的位置继续写入),所以再次写入100这个数据的之后,position=4变成了position=5

1.2.6 rewind():倒带(重新读取)

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带

@Test
public void testRewind() {
    IntBuffer buffer = IntBuffer.allocate(20);
    buffer.put(1);
    buffer.put(2);
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);

    // 调用flip,准备读取数据
    buffer.flip();
    System.out.println("调用flip之后:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    for (int i = 0; i < buffer.limit(); i++) {
        System.out.println(buffer.get());
    }
    // 读取之后的
    System.out.println("读取之后的三个属性信息:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    //
    buffer.rewind();
    System.out.println("rewind之后的三个属性信息:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    // 再次读取
    System.out.println("再次读取:");
    for (int i = 0; i < buffer.limit(); i++) {
        System.out.println(buffer.get());
    }
}

输出:

调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
1
2
3
4
5
读取之后的三个属性信息:
position = 5 ;capacity = 20 ;limit = 5
rewind之后的三个属性信息:
position = 0 ;capacity = 20 ;limit = 5
再次读取:
1
2
3
4
5

摘出核心的两个输出:

调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
rewind之后的三个属性信息:
position = 0 ;capacity = 20 ;limit = 5

调用rewind之后的三个属性信息,又回到了调用调用flip之后的值。

rewind ()方法主要是调整了缓冲区的position属性与mark属性,具体的调整规则如下:

  1. position重置为0,所以可以重读缓冲区中的所有数据。
  2. limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量。
  3. mark被清理,表示之前的临时位置不能再用了。

源码也很是简单:

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

1.2.7 mark()和reset()

  1. mark()方法将当前position的值保存起来放在mark属性中,让mark属性记住这个临时位置
  2. reset()方法将mark的值恢复到position中。
@Test
public void testMark(){
    IntBuffer buffer = IntBuffer.allocate(5);
    buffer.put(1);
    buffer.put(2);
    buffer.mark();
    buffer.put(3);
    buffer.put(4);
    buffer.put(5);
    // 输出数据
    Arrays.stream(buffer.array()).forEach(System.out::print);
    System.out.println("\nreset之前的三个属性信息:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
    // 调用reset
    buffer.reset();
    System.out.println("reset之后的三个属性信息:");
    System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());

    // 再次写入两个数据
    buffer.put(6);
    buffer.put(7);
    // 输出数据
    Arrays.stream(buffer.array()).forEach(System.out::print);
}

输出

12345
reset之前的三个属性信息:
position = 5 ;capacity = 5 ;limit = 5
reset之后的三个属性信息:
position = 2 ;capacity = 5 ;limit = 5
12675

分析:

  1. 当写入到2这个数据的时候,调用了mark方法,此时position=3,就会把position的值保存到mark属性中
  2. 紧接着又写入了3,4,5三个数据,此时position = 5
  3. 调用了reset方法,就会把position 恢复到之前保存的3
  4. 然后又写入了两个数据6,7,此时就会从position = 3开始写,所以就会把3和4给覆盖为6和7,所以最后buffer中的数据为:12675

2 使用Buffer类的基本步骤

(1)使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象。 (2)调用put()方法将数据写入缓冲区中。 (3)写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式。 (4)调用get()方法,可以从缓冲区中读取数据。 (5)读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入。

  • 以上就是《详解NIO Buffer类》的分享。
  • 也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。
  • 创作不易,你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值