大家好,我是大明哥,今天我们来看看 Buffer。
上面几篇文章详细介绍了 IO 相关的一些基本概念,如阻塞、非阻塞、同步、异步的区别,Reactor 模式、Proactor 模式。以下是这几篇文章的链接,有兴趣的同学可以阅读下:
- 【死磕NIO】— 阻塞、非阻塞、同步、异步,傻傻分不清楚
- 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?
- 【死磕 NIO】— Reactor 模式就一定意味着高性能吗?
- 【死磕 NIO】— Proactor模式是什么?很牛逼吗?
从这篇文章开始,我们将回归 NIO 方面的相关知识,首先从 NIO 的三大核心组件说起。
- Buffer
- Channel
- Selector
首先是 Buffer
Buffer
Buffer 是一个抽象类,主要用作缓冲区,其实质我们可以认为是一个可以写入数据,然后从中读取数据的内存块。这块内存被包装成 NIO Buffer 对象,并提供一系列的方法便于我们访问这块内存。
要理解 Buffer 的工作原理,首先就要理解它的 4 个索引:
- capacity:容量
- position:位置
- limit:界限
- mark:标记
capacity 则表示该 Buffer 的容量,而 position 和 limit 的含义取决于 Buffer 处于什么模式(读模式或者写模式),下图描述读写模式下这三种属性的含义
- capacity
capacity 表示容量,Buffer 是一个内存块,其存储数据的最大大小就是 capacity。我们不断地往 Buffer 中写入数据,当 Buffer 被写满后也就是存储的数据达到 capacity 了就需要将其清空,才能继续写入数据。
- position
position 的含义取决于 Buffer 处于写模式还是读模式:
- 如果是写模式,则写入的地方就是所谓的 position,其初始值是 0,最大值是 capacity - 1,当往 Buffer 中写入一个数据时,position 就会向前移动到下一个待写入的位置。
- 如果是读模式,则读取数据的地方就是 position。当执行
flip()
将 buffer 从写模式切换到读模式时,position 会被重置为 0,随着数据不断的读取,position 不断地向前移,直到 limit。 - limit
与 position 一样,limit 的含义也取决于 Buffer 处于何种模式:
- 写模式:当 Buffer 处于写模式时,limit 是指能够往 Buffer 中写入多少数据,其值等于 capacity
- 读模式:当 Buffer 处于读模式时,limit 表示能够从 Buffer 中最多能够读取多少数据出来,所以当 Buffer 从写模式切换到读模式时,limit 会被设置写模式下的 position 的值
- mark
mark 仅仅只是一个标识,可以通过 mark()
方法进行设置,设置值为当前的 position
Buffer 方法
Buffer 提供了一系列的方法用来操作它,比如 clear()
用来清空缓冲区,filp()
用来读切换等等方法,下面将依次演示 Buffer 的主要方法,包含从 Buffer 获取实例、写入数据、读取数据、重置等等一个系列的操作流程,同时将 position、limit 两个参数打印出来,便于我们更好地理解 Buffer。
allocate()
要获取一个 Buffer 对象,首先就要为期分配内存空间,使用 allocate()
方法分配内存空间,如下:
DoubleBuffer buffer = DoubleBuffer.allocate(10);
System.out.println("================= allocate 10 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
这里分配了 10 * sikeof(double)
字节的内存空间。需要注意的是 allocate()
里面参数并不是字节数,而是写入对象的数量,比如上面实例参数是 10 ,表明我们可以写 10 个 double 对象。
结果如下:
================= allocate 10 后 =================
capacity = 10
position = 0
limit = 10
此时,Buffer 的情况如下:
put()
调用 allocate()
分配内存后,得到 DoubleBuffer 实例对象,该对象目前处于写模式,我们可以通过 put()
方法向 Buffer 里面写入数据。
buffer.put(1);
buffer.put(2);
System.out.println("================= put 1、2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
调用 put()
往 DoubleBuffer 里面存放 2 个元素,此时,各自参数值如下:
================= put 1、2 后 =================
capacity = 10
position = 2
limit = 10
我们看到 position 的值变成了 2 ,指向第三个可以写入元素的位置。这个时候我们再写入 3 个元素:
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("================= put 3、4、5 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到结果如下:
================= put 3、4、5 后 =================
capacity = 10
position = 5
limit = 10
此时,position 的值变成 5 ,指向第 6 个可以写入元素的位置。
该 Buffer 的情况如下:
flip()
调用 put()
方法向 Buffer 中存储数据后,这时 Buffer 仍然处于写模式状态,在写模式状态下我们是不能直接从 Buffer 中读取数据的,需要调用 flip()
方法将 Buffer 从写模式切换为读模式。
buffer.flip();
System.out.println("================= flip 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到的结果如下:
================= flip 后 =================
capacity = 10
position = 0
limit = 5
调用 flip()
方法将 Buffer 从写模式切换为读模式后,Buffer 的参数发生了微秒的变化:position = 0,limit = 5。前面说过在读模式下,limit 代表是 Buffer 的可读长度,它等于写模式下的 position,而 position 则是读的位置。
flip()
方法主要是将 Buffer 从写模式切换为读模式,其调整的规则如下:
- 设置可读的长度 limit。将写模式写的 Buffer 中内容的最后位置 position 值变成读模式下的 limit 位置值,新的 limit 值作为读越界位置
- 设置读的起始位置。将 position 的值设置为 0 ,表示从 0 位置处开始读
- 如果之前有 mark 保存的标记位置,也需要消除,因为那是写模式下的 mark 标记
调动 flip()
后,该 Buffer 情况如下:
get()
调用 flip()
将 Buffer 切换为读模式后,就可以调用 get()
方法读取 Buffer 中的数据了,get()
读取数据很简单,每次从 position 的位置读取一个数据,并且将 position 向前移动 1 位。如下:
System.out.println("读取第 1 个位置的数据:" + buffer.get());
System.out.println("读取第 2 个位置的数据:" + buffer.get());
System.out.println("================= get 2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
连续调用 2 次 get()
方法,输出结果:
读取第 1 个位置的数据:1.0
读取第 2 个位置的数据:2.0
================= get 2 后 =================
capacity = 10
position = 2
limit = 5
position 的值变成了 2 ,表明它向前移动了 2 位,此时,Buffer 如下:
我们知道 limit 表明当前 Buffer 最大可读位置,buffer 也是一边读,position 位置一边往前移动,那如果越界读取呢?
System.out.println("读取第 3 个位置的数据:" + buffer.get());
System.out.println("读取第 4 个位置的数据:" + buffer.get());
System.out.println("读取第 5 个位置的数据:" + buffer.get());
System.out.println("读取第 6 个位置的数据:" + buffer.get());
System.out.println("读取第 7 个位置的数据:" + buffer.get());
limit = 5,6 、7 位置明显越界了,如果越界读取,Buffer 会抛出 BufferUnderflowException,如下:
读取第 3 个位置的数据:3.0
读取第 4 个位置的数据:4.0
读取第 5 个位置的数据:5.0
Exception in thread "main" java.nio.BufferUnderflowException
at java.nio.Buffer.nextGetIndex(Buffer.java:500)
at java.nio.HeapDoubleBuffer.get(HeapDoubleBuffer.java:135)
at com.chenssy.study.nio.BufferTest.main(BufferTest.java:48)
rewind()
position 是随着读取的进度一直往前移动的,那如果我想在读取一遍数据呢?使用 rewind()
方法,可以进行重复读。rewind()
也叫做倒带,就想播放磁带一样,倒回去重新读。
buffer.rewind();
System.out.println("================= rewind 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果:
================= rewind 后 =================
capacity = 10
position = 0
limit = 5
可以看到,仅仅只是将 position 的值设置为了 0,limit 的值保持不变。
clear() 和 compact()
flip()
方法用于将 Buffer 从写模式切换到读模式,那怎么将 Buffer 从读模式切换至写模式呢?可以调用 clear()
和 compact()
两个方法。
- clear()
buffer.clear();
System.out.println("================= clear 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果如下:
================= clear 后 =================
capacity = 10
position = 0
limit = 10
调用 clear()
后,我们发现 position 的值变成了 0,limit 值变成了 10,也就是 Buffer 被清空了,回归到最初始状态。但是里面的数据仍然是存在的,只是没有标记哪些数据是已读,哪些为未读。
- compact()
compact()
方法也可以将 Buffer 从读模式切换到写模式,它跟 clear()
有一些区别。
buffer.compact();
System.out.println("================= compact 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果如下:
================= compact 后 =================
capacity = 10
position = 3
limit = 10
可以看到 position 的值为 3,它与 clear()
区别就在于,它会将所有未读的数据全部复制到 Buffer 的前面(5次put()
,两次 get()
),将 position 设置到这些数据后面,所以此时是从未读的数据后面开始写入新的数据,Buffer 情况如下:
mark() 和 reset()
调用 mark()
方法可以标志一个指定的位置(即设置 mark 的值),之后调用 reset()
时,position 又会回到之前标记的位置。
通过上面的步骤演示,我想小伙伴基本上已经掌握了 Buffer 的使用方法,这里简要总结下,使用 Buffer 的步骤如下:
- 将数据写入 Buffer 中
- 调用
flip()
方法,将 Buffer 切换为读模式 - 从 Buffer 中读取数据
- 调用
clear()
或者compact()
方法将 Buffer 切换为写模式
Buffer 的类型
在 NIO 中主要有 8 中 Buffer,分别如下:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- MappedByteBuffer
其 UML 类图如下:
这些不同的 Buffer 类型代表了不同的数据类型,使得可以通过 Buffer 直接操作如 char、short 等类型的数据而不是字节数据。这些 Buffer 基本上覆盖了所有能从 IO 中传输的 Java 基本数据类型,其中 MappedByteBuffer 是专门用于内存映射的的一种 ByteBuffer,后续会专门介绍。
到这里 Buffer 也就介绍完毕了,下篇文章将介绍它的协作者 Channel。