【死磕 NIO】— 深入分析Buffer

2023-11-13

大家好,我是大明哥,今天我们来看看 Buffer。

上面几篇文章详细介绍了 IO 相关的一些基本概念,如阻塞、非阻塞、同步、异步的区别,Reactor 模式、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 的步骤如下:

  1. 将数据写入 Buffer 中

  2. 调用 flip() 方法,将 Buffer 切换为读模式

  3. 从 Buffer 中读取数据

  4. 调用 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。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【死磕 NIO】— 深入分析Buffer 的相关文章

随机推荐

  • 在职场中哪些人不会被企业团队选用?在企业团队中哪些人不能用?

    企业团队离不开员工的打拼 那么现在人才济济的社会 企业团队中那些人是不能用的 小人不能用 当今社会我们评论一个人的好坏 首先看到的是他的道德品质 当一个人的道德品质不行 道德品行很差时 我们统称这类人为小人 都说远小人 在职场中也一样 小人
  • 华为OD机试 C++【TLV解析】

    题目 你收到了一串由两端设备传递的TLV格式的消息 现在你需要根据这串消息生成一个对应的 tag length valueOffset 列表 详细说明 这串消息其实是由许多小组成的 每一小组里包含了tag length value 其中 t
  • 关于element-ui el-cascader 级联选择器 单独选择任意一级选项,去掉单选按钮(记录一下)

    效果如下 分类 代码如下 单独选择任意一项属性 checkStrictly 绑定的是分类 id popper class自定义类名
  • SpringBoot--基础--05--错误处理

    SpringBoot 基础 05 错误处理 一 原理 1 1 自动配置类 ErrorMvcAutoConfiguration 1 2 一但系统出现4xx或者5xx之类的错误 ErrorPageCustomizer就会生效 定制错误的响应规则
  • 13个SQL优化技巧

    1 避免无计划的全表扫描 如下情况进行全表扫描 该表无索引 对返回的行无人和限制条件 无Where子句 对于索引主列 索引的第一列 无限制条件 对索引主列的条件含在表达式中 对索引主列的限制条件是is not null或 对索引主列的限制条
  • 国内IoT云平台横向对比

    三年过去了 目前各IoT平台功能有了极大的丰富和优化 行业头部玩家也逐渐浮出水面 目前市场的排名如下 阿里 小米 京东 百度 涂鸦 这几家以开放的生态 接入了千万甚至亿级设备 传统设备厂家如美的 海尔也在积极搭建自己的物联网平台 物联网已然
  • 华为天才少年谢凌曦分享了万字长文,阐述了关于视觉识别领域发展的个人观点...

    作者 谢凌曦 编辑 桃子 报道 新智元 计算机视觉识别领域的发展如何 华为天才少年谢凌曦分享了万字长文 阐述了个人对其的看法 最近 我参加了几个高强度的学术活动 包括CCF计算机视觉专委会的闭门研讨会和VALSE线下大会 经过与其他学者的交
  • 【软件分析/静态分析】学习笔记01——Introduction

    课程链接 李樾老师和谭天老师的 南京大学 软件分析 课程01 Introduction 哔哩哔哩 bilibili 目录 一 静态程序分析介绍 1 1 PL and Static Analysis 程序语言和静态分析 1 2 为什么要学 S
  • HTTP学习(五)实体、编码

    HTTP作为现在非常重要的协议 需要仔细梳理一下 本次学习知识点来自于 HTTP权威指南 只是文中知识点罗列 算是读书笔记 请有兴趣的读者购买 HTTP权威指南 完整阅读 一 实体 每天都有数以亿计的各种媒体对象经由 HTTP 传送 如图像
  • AMD CPU针脚弯曲无工具 完美修好

    b350m pro4 r7 3700 Q 电脑cpu针脚不小心弯了怎么办 今天在清理机箱的时候 把风扇取下来的时候 cpu连着风扇一起取下来了 然后因为我的错误操作 cpu有一排针脚都弯曲 对角针脚有四五跟接近45度弯曲 见图 解决工具 针
  • TreeMap的应用

    TreeMap的demo package dailyTest import java util Comparator import java util TreeMap author createtime 2023 9 12 19 56 pu
  • C# 学习笔记(四)数据类型

    C 学习笔记 四 数据类型 开篇语 C 都有哪些数据类型呢 前面几次学习里我们已经遇到过的几种数据类型有 string 字符串类型 double 双精度类型 抽象类型 即Class类型 具体的某个类 在 C 中 变量分为以下几种类型 值类型
  • Transformer详解

    Transformer 什么是transformer 为什么需要用transformer encoder sub encoder block multi head self attention FFN input decoder input
  • 用Vue搭建一个大屏数据可视化页面实战一(Vue实战系列)

    一 从网上找一个喜欢的图 1 首先大屏要先解决屏幕适配的问题 这里用viewport的方案 使用postcss px to viewport插件 在vue项目里新建postcss config js配置一下postcss px to vie
  • Python基础知识(九):文件与文件系统

    1 文件与文件系统 打开文件 open file mode r buffering None encoding None errors None newline None closefd True Open file and return
  • 等保2.0安全通用要求每级控制点统计

    等保2 0安全通用要求每级控制点的变化如下表格
  • 自动化测试项目实战经验附视频以及源码【商城项目,app项目,电商项目,银行项目,医药项目,金融项目】

    最近收到许多自学自动化测试的小伙伴私信 学习了理论知识后 却没有合适的练手项目 测试本身是一个技术岗位 如果只知道理论 没有实战经验 在面试中很难说服面试官 比如什么场景下需要添加显示等待 什么时候元素定位可以写得更优雅 如何做断言等 这些
  • 问题:TypeError: Descriptors cannot not be created directly.解决方法

    运行代码时提示如下报错 解决上面问题 直接在控制台中输入下面命令重新安装protobuf即可 如下 pip install protobuf 3 19 0 以上就是解决TypeError Descriptors cannot not be
  • 2022-02-22每日刷题打卡

    2022 02 22每日刷题打卡 一本通 动态规划 1268 例9 12 完全背包问题 题目描述 设有n种物品 每种物品有一个重量及一个价值 但每种物品的数量是无限的 同时有一个背包 最大载重量为M 今从n种物品中选取若干件 同一种物品可以
  • 【死磕 NIO】— 深入分析Buffer

    大家好 我是大明哥 今天我们来看看 Buffer 上面几篇文章详细介绍了 IO 相关的一些基本概念 如阻塞 非阻塞 同步 异步的区别 Reactor 模式 Proactor 模式 以下是这几篇文章的链接 有兴趣的同学可以阅读下 死磕NIO