-
2022-03-16 15:05:33
目录
一、缓冲分析效果图
二、部分关键代码
1、点缓冲分析
/** * @author: * @Date: 2022-03-15 18:00:49 * @note: 注意事项 * @description: 创建点的缓冲区 */ createPointBuffer() { this.addPoint() const coordinate = this.cartesianToLatlng(this.positions[0]).slice(0, 2); let pointF = turf.point(coordinate) let buffered = turf.buffer(pointF, this.radius) let coordinates = buffered.geometry.coordinates; let points = coordinates[0] let degreesArray = this.pointsToDegreesArray(points); this.createBuffer(Cesium.Cartesian3.fromDegreesArray(degreesArray)) }
2、线缓冲分析
/** * @author: * @Date: 2022-03-15 18:00:00 * @note: 注意事项 * @description: 创建线缓冲区 */ createLineBuffer() { var points = this.getLngLats(); // 坐标数组 var polylineF = turf.lineString(points); var bufferd = turf.buffer(polylineF, this.radius); var coordinates = bufferd.geometry.coordinates; points = coordinates[0] var degreesArray = this.pointsToDegreesArray(points) this.createBuffer(Cesium.Cartesian3.fromDegreesArray(degreesArray)) }
3、面缓冲分析
/** * @author: * @Date: 2022-03-15 17:57:41 * @note: 注意事项 * @description: 计算多边形缓冲区 */ createPolygonBuffer() { var points = this.getLngLats(); // 坐标数组 points.push(points[0]) var polygonF = turf.polygon([points]); var bufferd = turf.buffer(polygonF, this.radius); var coordinates = bufferd.geometry.coordinates; points = coordinates[0] var degreesArray = this.pointsToDegreesArray(points) this.createBuffer(Cesium.Cartesian3.fromDegreesArray(degreesArray)) }
4、生成缓冲区
/** * @author: * @Date: 2022-03-15 18:01:35 * @note: 注意事项 * @description: 生成缓冲区坐标数据 */ createBuffer(array) { const bufferPolygon = this.viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(array), material: Cesium.Color.RED.withAlpha(0.5), classificationType: Cesium.ClassificationType.BOTH }, }); this.bufferEntities.push(bufferPolygon) }
三、vue中调用
1、引入缓冲分析对应类
import bufferAnalysisUtil from '../../../../bufferAnalysisUtil'
2、点线面的缓冲分析调用方法
/** * @author: * @Date: 2022-03-16 14:29:53 * @note: 注意事项 * @description: 缓冲分析 */ bufferTool(){ switch (this.bufferLabel) { case 'Point': m_bufferAnalysis.active({type:'Point',radius: Number(this.input) / 1000}) break; case 'Line': m_bufferAnalysis.active({type:'Line',radius: Number(this.input) / 1000}) break; case 'Polygon': m_bufferAnalysis.active({type:'Polygon',radius: Number(this.input) / 1000}) break; case 'clear': m_bufferAnalysis.deactive() break; } },
四、缓冲分析类源码
更多相关内容 -
JS判断页面加载状态以及添加遮罩和缓冲动画的代码
2020-10-27 22:15:53JS判断页面加载状态以及添加遮罩和缓冲动画的代码废话少说,直接贴代码!有注释 -
易语言双缓冲绘图
2020-08-20 21:52:36易语言双缓冲绘图系统结构:不同字体文本测量,相同字体文本测量,判断字符编码类型,判断大小写,判断是否汉字,判断是否希腊字符,判断是否数字,判断字符类型,判断是否小写罗马数字 -
基于高效布尔运算的三维矢量缓冲区算法 (2012年)
2021-05-15 03:34:43算法通过对布尔运算进行深入研究的基础上,描述了布尔运算算法,分析了算法复杂度,提出了一种提高布尔运算稳定性的新方法。该稳定性方法采用了拓扑关系完整性、逻辑判断统一性和运算容差统一性这3个规则来避免运算... -
【Java基础-3】吃透Java IO:字节流、字符流、缓冲流
2020-09-23 20:12:33什么是Java-IO?字符流和字节流的区别与适用场景是什么?缓冲流到底实现了什么?如何高效地读写文件? 本文用大量的示例图和实例,带你吃透Java IO。Java IO流
前言
有人曾问fastjson的作者(阿里技术专家高铁):“你开发fastjson,没得到什么好处,反而挨了骂背了锅,这种事情你为什么要做呢?”
高铁答道:“因为热爱本身,就是奖励啊!”
这个回答顿时触动了我。想想自己,又何尝不是如此。写作是个痛苦的过程,用心写作就更加煎熬,需字字斟酌,反复删改才有所成。然而,当一篇篇精良文章出自己手而呈现眼前时,那些痛苦煎熬就都那么值得。如果这些博文能有幸得大家阅读和认可,就更加是莫大的鼓舞了。技术人的快乐就是可以这么纯粹和简单。
点波关注不迷路,一键三连好运连连!
IO流是Java中的一个重要构成部分,也是我们经常打交道的。这篇关于Java IO的博文干货满满,堪称全网前三(请轻喷!)
下面几个问题(问题还会继续补充),如果你能对答如流,那么恭喜你,IO知识掌握得很好,可以立即关闭文章。反之,你可以在后面得文章中寻找答案。
- Java IO流有什么特点?
- Java IO流分为几种类型?
- 字节流和字符流的关系与区别?
- 字符流是否使用了缓冲?
- 缓冲流的效率一定高吗?为什么?
- 缓冲流体现了Java中的哪种设计模式思想?
- 为什么要实现序列化?如何实现序列化?
- 序列化数据后,再次修改类文件,读取数据会出问题,如何解决呢?
1 初识Java IO
IO,即
in
和out
,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。Java 中是通过流处理IO 的,那么什么是流?
流(
Stream
),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
一般来说关于流的特性有下面几点:
- 先进先出:最先写入输出流的数据最先被输入流读取到。
- 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(
RandomAccessFile
除外) - 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
1.1 IO流分类
IO流主要的分类方式有以下3种:
- 按数据流的方向:输入流、输出流
- 按处理数据单位:字节流、字符流
- 按功能:节点流、处理流
1、输入流与输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文件是输出流,这点很容易搞反。
2、字节流与字符流字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
为什么要有字符流?
Java中字符是采用Unicode标准,Unicode 编码中,一个英文字母或一个中文汉字为两个字节。
而在UTF-8编码中,一个中文字符是3个字节。例如下面图中,“云深不知处”5个中文对应的是15个字节:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124
那么问题来了,如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。为了更方便地处理中文这些字符,Java就推出了字符流。
字节流和字符流的其他区别:
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。
以写文件为例,我们查看字符流的源码,发现确实有利用到缓冲区:
3、节点流和处理流
节点流:直接操作数据读写的流类,比如
FileInputStream
处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,例如
BufferedInputStream
(缓冲字节流)处理流和节点流应用了Java的装饰者设计模式。
下图就很形象地描绘了节点流和处理流,处理流是对节点流的封装,最终的数据处理还是由节点流完成的。
在诸多处理流中,有一个非常重要,那就是缓冲流。我们知道,程序与磁盘的交互相对于内存运算是很慢的,容易成为程序的性能瓶颈。减少程序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
联想一下生活中的例子,我们搬砖的时候,一块一块地往车上装肯定是很低效的。我们可以使用一个小推车,先把砖装到小推车上,再把这小推车推到车前,把砖装到车上。这个例子中,小推车可以视为缓冲区,小推车的存在,减少了我们装车次数,从而提高了效率。
需要注意的是,缓冲流效率一定高吗?不一定,某些情形下,缓冲流效率反而更低,具体请见IO流效率对比。完整的IO分类图如下:
1.2 案例实操
接下来,我们看看如何使用Java IO。
文本读写的例子,也就是文章开头所说的,将“松下问童子,言师采药去。只在此山中,云深不知处。”写入本地文本,然后再从文件读取内容并输出到控制台。
1、FileInputStream、FileOutputStream(字节流)
字节流的方式效率较低,不建议使用
public class IOTest { public static void main(String[] args) throws IOException { File file = new File("D:/test.txt"); write(file); System.out.println(read(file)); } public static void write(File file) throws IOException { OutputStream os = new FileOutputStream(file, true); // 要写入的字符串 String string = "松下问童子,言师采药去。只在此山中,云深不知处。"; // 写入文件 os.write(string.getBytes()); // 关闭流 os.close(); } public static String read(File file) throws IOException { InputStream in = new FileInputStream(file); // 一次性取多少个字节 byte[] bytes = new byte[1024]; // 用来接收读取的字节数组 StringBuilder sb = new StringBuilder(); // 读取到的字节数组长度,为-1时表示没有数据 int length = 0; // 循环取数据 while ((length = in.read(bytes)) != -1) { // 将读取的内容转换成字符串 sb.append(new String(bytes, 0, length)); } // 关闭流 in.close(); return sb.toString(); } }
2、BufferedInputStream、BufferedOutputStream(缓冲字节流)
缓冲字节流是为高效率而设计的,真正的读写操作还是靠
FileOutputStream
和FileInputStream
,所以其构造方法入参是这两个类的对象也就不奇怪了。public class IOTest { public static void write(File file) throws IOException { // 缓冲字节流,提高了效率 BufferedOutputStream bis = new BufferedOutputStream(new FileOutputStream(file, true)); // 要写入的字符串 String string = "松下问童子,言师采药去。只在此山中,云深不知处。"; // 写入文件 bis.write(string.getBytes()); // 关闭流 bis.close(); } public static String read(File file) throws IOException { BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file)); // 一次性取多少个字节 byte[] bytes = new byte[1024]; // 用来接收读取的字节数组 StringBuilder sb = new StringBuilder(); // 读取到的字节数组长度,为-1时表示没有数据 int length = 0; // 循环取数据 while ((length = fis.read(bytes)) != -1) { // 将读取的内容转换成字符串 sb.append(new String(bytes, 0, length)); } // 关闭流 fis.close(); return sb.toString(); } }
3、InputStreamReader、OutputStreamWriter(字符流)
字符流适用于文本文件的读写,
OutputStreamWriter
类其实也是借助FileOutputStream
类实现的,故其构造方法是FileOutputStream
的对象public class IOTest { public static void write(File file) throws IOException { // OutputStreamWriter可以显示指定字符集,否则使用默认字符集 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8"); // 要写入的字符串 String string = "松下问童子,言师采药去。只在此山中,云深不知处。"; osw.write(string); osw.close(); } public static String read(File file) throws IOException { InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8"); // 字符数组:一次读取多少个字符 char[] chars = new char[1024]; // 每次读取的字符数组先append到StringBuilder中 StringBuilder sb = new StringBuilder(); // 读取到的字符数组长度,为-1时表示没有数据 int length; // 循环取数据 while ((length = isr.read(chars)) != -1) { // 将读取的内容转换成字符串 sb.append(chars, 0, length); } // 关闭流 isr.close(); return sb.toString() } }
4、字符流便捷类
Java提供了
FileWriter
和FileReader
简化字符流的读写,new FileWriter
等同于new OutputStreamWriter(new FileOutputStream(file, true))
public class IOTest { public static void write(File file) throws IOException { FileWriter fw = new FileWriter(file, true); // 要写入的字符串 String string = "松下问童子,言师采药去。只在此山中,云深不知处。"; fw.write(string); fw.close(); } public static String read(File file) throws IOException { FileReader fr = new FileReader(file); // 一次性取多少个字节 char[] chars = new char[1024]; // 用来接收读取的字节数组 StringBuilder sb = new StringBuilder(); // 读取到的字节数组长度,为-1时表示没有数据 int length; // 循环取数据 while ((length = fr.read(chars)) != -1) { // 将读取的内容转换成字符串 sb.append(chars, 0, length); } // 关闭流 fr.close(); return sb.toString(); } }
5、BufferedReader、BufferedWriter(字符缓冲流)
public class IOTest { public static void write(File file) throws IOException { // BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(new // FileOutputStream(file, true), "UTF-8")); // FileWriter可以大幅度简化代码 BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 要写入的字符串 String string = "松下问童子,言师采药去。只在此山中,云深不知处。"; bw.write(string); bw.close(); } public static String read(File file) throws IOException { BufferedReader br = new BufferedReader(new FileReader(file)); // 用来接收读取的字节数组 StringBuilder sb = new StringBuilder(); // 按行读数据 String line; // 循环取数据 while ((line = br.readLine()) != null) { // 将读取的内容转换成字符串 sb.append(line); } // 关闭流 br.close(); return sb.toString(); } }
2 IO流对象
第一节中,我们大致了解了IO,并完成了几个案例,但对IO还缺乏更详细的认知,那么接下来我们就对Java IO细细分解,梳理出完整的知识体系来。
Java种提供了40多个类,我们只需要详细了解一下其中比较重要的就可以满足日常应用了。
2.1 File类
File
类是用来操作文件的类,但它不能操作文件中的数据。public class File extends Object implements Serializable, Comparable<File>
File
类实现了Serializable
、Comparable<File>
,说明它是支持序列化和排序的。File类的构造方法
方法名 说明 File(File parent, String child)
根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。 File(String pathname)
通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。 File(String parent, String child)
根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。 File(URI uri)
通过将给定的 file: URI 转换为一个抽象路径名来创建一个新的 File 实例。 File类的常用方法
方法 说明 createNewFile()
当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。 delete()
删除此抽象路径名表示的文件或目录。 exists()
测试此抽象路径名表示的文件或目录是否存在。 getAbsoluteFile()
返回此抽象路径名的绝对路径名形式。 getAbsolutePath()
返回此抽象路径名的绝对路径名字符串。 length()
返回由此抽象路径名表示的文件的长度。 mkdir()
创建此抽象路径名指定的目录。 File类使用实例
public class FileTest { public static void main(String[] args) throws IOException { File file = new File("C:/Mu/fileTest.txt"); // 判断文件是否存在 if (!file.exists()) { // 不存在则创建 file.createNewFile(); } System.out.println("文件的绝对路径:" + file.getAbsolutePath()); System.out.println("文件的大小:" + file.length()); // 刪除文件 file.delete(); } }
2.2 字节流
InputStream
与OutputStream
是两个抽象类,是字节流的基类,所有具体的字节流实现类都是分别继承了这两个类。以
InputStream
为例,它继承了Object
,实现了Closeable
public abstract class InputStream extends Object implements Closeable
InputStream
类有很多的实现子类,下面列举了一些比较常用的:
详细说明一下上图中的类:InputStream
:InputStream
是所有字节输入流的抽象基类,前面说过抽象类不能被实例化,实际上是作为模板而存在的,为所有实现类定义了处理输入流的方法。FileInputSream
:文件输入流,一个非常重要的字节输入流,用于对文件进行读取操作。PipedInputStream
:管道字节输入流,能实现多线程间的管道通信。ByteArrayInputStream
:字节数组输入流,从字节数组(byte[])中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去。FilterInputStream
:装饰者类,具体的装饰者继承该类,这些类都是处理类,作用是对节点类进行封装,实现一些特殊功能。DataInputStream
:数据输入流,它是用来装饰其它输入流,作用是“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。BufferedInputStream
:缓冲流,对节点流进行装饰,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送,效率更高。ObjectInputStream
:对象输入流,用来提供对基本数据或对象的持久存储。通俗点说,也就是能直接传输对象,通常应用在反序列化中。它也是一种处理流,构造器的入参是一个InputStream
的实例对象。
OutputStream
类继承关系图:
OutputStream
类继承关系与InputStream
类似,需要注意的是PrintStream
.2.3 字符流
与字节流类似,字符流也有两个抽象基类,分别是
Reader
和Writer
。其他的字符流实现类都是继承了这两个类。以
Reader
为例,它的主要实现子类如下图:
各个类的详细说明:InputStreamReader
:从字节流到字符流的桥梁(InputStreamReader
构造器入参是FileInputStream
的实例对象),它读取字节并使用指定的字符集将其解码为字符。它使用的字符集可以通过名称指定,也可以显式给定,或者可以接受平台的默认字符集。BufferedReader
:从字符输入流中读取文本,设置一个缓冲区来提高效率。BufferedReader
是对InputStreamReader
的封装,前者构造器的入参就是后者的一个实例对象。FileReader
:用于读取字符文件的便利类,new FileReader(File file)
等同于new InputStreamReader(new FileInputStream(file, true),"UTF-8")
,但FileReader
不能指定字符编码和默认字节缓冲区大小。PipedReader
:管道字符输入流。实现多线程间的管道通信。CharArrayReader
:从Char
数组中读取数据的介质流。StringReader
:从String
中读取数据的介质流。
Writer
与Reader
结构类似,方向相反,不再赘述。唯一有区别的是,Writer
的子类PrintWriter
。2.4 序列化
待续…
3 IO流方法
3.1 字节流方法
字节输入流
InputStream
主要方法:read()
:从此输入流中读取一个数据字节。read(byte[] b)
:从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。read(byte[] b, int off, int len)
:从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。close()
:关闭此输入流并释放与该流关联的所有系统资源。
字节输出流
OutputStream
主要方法:write(byte[] b)
:将 b.length 个字节从指定 byte 数组写入此文件输出流中。write(byte[] b, int off, int len)
:将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。write(int b)
:将指定字节写入此文件输出流。close()
:关闭此输入流并释放与该流关联的所有系统资源。
3.2 字符流方法
字符输入流
Reader
主要方法:read()
:读取单个字符。read(char[] cbuf)
:将字符读入数组。read(char[] cbuf, int off, int len)
: 将字符读入数组的某一部分。read(CharBuffer target)
:试图将字符读入指定的字符缓冲区。flush()
:刷新该流的缓冲。close()
:关闭此流,但要先刷新它。
字符输出流
Writer
主要方法:write(char[] cbuf)
:写入字符数组。write(char[] cbuf, int off, int len)
:写入字符数组的某一部分。write(int c)
:写入单个字符。write(String str)
:写入字符串。write(String str, int off, int len)
:写入字符串的某一部分。flush()
:刷新该流的缓冲。close()
:关闭此流,但要先刷新它。
另外,字符缓冲流还有两个独特的方法:
BufferedWriter
类newLine()
:写入一个行分隔符。这个方法会自动适配所在系统的行分隔符。BufferedReader
类readLine()
:读取一个文本行。
4 附加内容
4.1 位、字节、字符
字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位。
字符(Character)计算机中使用的字母、数字、字和符号,比如’A’、‘B’、’$’、’&'等。
一般在英文状态下一个字母或字符占用一个字节,一个汉字用两个字节表示。
字节与字符:
- ASCII 码中,一个英文字母(不分大小写)为一个字节,一个中文汉字为两个字节。
- UTF-8 编码中,一个英文字为一个字节,一个中文为三个字节。
- Unicode 编码中,一个英文为一个字节,一个中文为两个字节。
- 符号:英文标点为一个字节,中文标点为两个字节。例如:英文句号 . 占1个字节的大小,中文句号 。占2个字节的大小。
- UTF-16 编码中,一个英文字母字符或一个汉字字符存储都需要 2 个字节(Unicode 扩展区的一些汉字存储需要 4 个字节)。
- UTF-32 编码中,世界上任何字符的存储都需要 4 个字节。
4.2 IO流效率对比
首先,对比下普通字节流和缓冲字节流的效率:
public class MyTest { public static void main(String[] args) throws IOException { File file = new File("C:/Mu/test.txt"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 3000000; i++) { sb.append("abcdefghigklmnopqrstuvwsyz"); } byte[] bytes = sb.toString().getBytes(); long start = System.currentTimeMillis(); write(file, bytes); long end = System.currentTimeMillis(); long start2 = System.currentTimeMillis(); bufferedWrite(file, bytes); long end2 = System.currentTimeMillis(); System.out.println("普通字节流耗时:" + (end - start) + " ms"); System.out.println("缓冲字节流耗时:" + (end2 - start2) + " ms"); } // 普通字节流 public static void write(File file, byte[] bytes) throws IOException { OutputStream os = new FileOutputStream(file); os.write(bytes); os.close(); } // 缓冲字节流 public static void bufferedWrite(File file, byte[] bytes) throws IOException { BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(file)); bo.write(bytes); bo.close(); } }
运行结果:
普通字节流耗时:250 ms 缓冲字节流耗时:268 ms
这个结果让我大跌眼镜,不是说好缓冲流效率很高么?要知道为什么,只能去源码里找答案了。翻看字节缓冲流的
write
方法:public synchronized void write(byte b[], int off, int len) throws IOException { if (len >= buf.length) { /* If the request length exceeds the size of the output buffer, flush the output buffer and then write the data directly. In this way buffered streams will cascade harmlessly. */ flushBuffer(); out.write(b, off, len); return; } if (len > buf.length - count) { flushBuffer(); } System.arraycopy(b, off, buf, count, len); count += len; }
注释里说得很明白:如果请求长度超过输出缓冲区的大小,刷新输出缓冲区,然后直接写入数据。这样,缓冲流将无害地级联。
但是,至于为什么这么设计,我没有想明白,有哪位明白的大佬可以留言指点一下。
基于上面的情形,要想对比普通字节流和缓冲字节流的效率差距,就要避免直接读写较长的字符串,于是,设计了下面这个对比案例:用字节流和缓冲字节流分别复制文件。
public class MyTest { public static void main(String[] args) throws IOException { File data = new File("C:/Mu/data.zip"); File a = new File("C:/Mu/a.zip"); File b = new File("C:/Mu/b.zip"); StringBuilder sb = new StringBuilder(); long start = System.currentTimeMillis(); copy(data, a); long end = System.currentTimeMillis(); long start2 = System.currentTimeMillis(); bufferedCopy(data, b); long end2 = System.currentTimeMillis(); System.out.println("普通字节流耗时:" + (end - start) + " ms"); System.out.println("缓冲字节流耗时:" + (end2 - start2) + " ms"); } // 普通字节流 public static void copy(File in, File out) throws IOException { // 封装数据源 InputStream is = new FileInputStream(in); // 封装目的地 OutputStream os = new FileOutputStream(out); int by = 0; while ((by = is.read()) != -1) { os.write(by); } is.close(); os.close(); } // 缓冲字节流 public static void bufferedCopy(File in, File out) throws IOException { // 封装数据源 BufferedInputStream bi = new BufferedInputStream(new FileInputStream(in)); // 封装目的地 BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(out)); int by = 0; while ((by = bi.read()) != -1) { bo.write(by); } bo.close(); bi.close(); } }
运行结果:
普通字节流耗时:184867 ms 缓冲字节流耗时:752 ms
这次,普通字节流和缓冲字节流的效率差异就很明显了,达到了245倍。
再看看字符流和缓冲字符流的效率对比:
public class IOTest { public static void main(String[] args) throws IOException { // 数据准备 dataReady(); File data = new File("C:/Mu/data.txt"); File a = new File("C:/Mu/a.txt"); File b = new File("C:/Mu/b.txt"); File c = new File("C:/Mu/c.txt"); long start = System.currentTimeMillis(); copy(data, a); long end = System.currentTimeMillis(); long start2 = System.currentTimeMillis(); copyChars(data, b); long end2 = System.currentTimeMillis(); long start3 = System.currentTimeMillis(); bufferedCopy(data, c); long end3 = System.currentTimeMillis(); System.out.println("普通字节流1耗时:" + (end - start) + " ms,文件大小:" + a.length() / 1024 + " kb"); System.out.println("普通字节流2耗时:" + (end2 - start2) + " ms,文件大小:" + b.length() / 1024 + " kb"); System.out.println("缓冲字节流耗时:" + (end3 - start3) + " ms,文件大小:" + c.length() / 1024 + " kb"); } // 普通字符流不使用数组 public static void copy(File in, File out) throws IOException { Reader reader = new FileReader(in); Writer writer = new FileWriter(out); int ch = 0; while ((ch = reader.read()) != -1) { writer.write((char) ch); } reader.close(); writer.close(); } // 普通字符流使用字符流 public static void copyChars(File in, File out) throws IOException { Reader reader = new FileReader(in); Writer writer = new FileWriter(out); char[] chs = new char[1024]; while ((reader.read(chs)) != -1) { writer.write(chs); } reader.close(); writer.close(); } // 缓冲字符流 public static void bufferedCopy(File in, File out) throws IOException { BufferedReader br = new BufferedReader(new FileReader(in)); BufferedWriter bw = new BufferedWriter(new FileWriter(out)); String line = null; while ((line = br.readLine()) != null) { bw.write(line); bw.newLine(); bw.flush(); } // 释放资源 bw.close(); br.close(); } // 数据准备 public static void dataReady() throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 600000; i++) { sb.append("abcdefghijklmnopqrstuvwxyz"); } OutputStream os = new FileOutputStream(new File("C:/Mu/data.txt")); os.write(sb.toString().getBytes()); os.close(); System.out.println("完毕"); } }
运行结果:
普通字符流1耗时:1337 ms,文件大小:15234 kb 普通字符流2耗时:82 ms,文件大小:15235 kb 缓冲字符流耗时:205 ms,文件大小:15234 kb
测试多次,结果差不多,可见字符缓冲流效率上并没有明显提高,我们更多的是要使用它的
readLine()
和newLine()
方法。4.3 NIO
待续…
-
浅谈缓冲的理论与实践
2022-03-23 15:53:30本文介绍了缓冲(Buffer)在Java开发中的常见用法和注意事项。博客主页:https://tomcat.blog.csdn.net
博主昵称:农民工老王
主要领域:Java、Linux、K8S
期待大家的关注💖点赞👍收藏⭐留言💬
本文将详细介绍“缓冲”这个优化手段,之前在 Java性能优化的七个方向的复用优化中便提到过“缓冲”,你可以回看复习一下。
深入理解缓冲的本质
缓冲(Buffer)通过对数据进行暂存,然后批量进行传输或者操作,多采用顺序方式,来缓解不同设备之间次数频繁但速度缓慢的随机读写。
你可以把缓冲区,想象成一个蓄水池。放水的水龙头一直开着,如果池子里有水,它就以恒定的速度流淌,不需要暂停;供水的水龙头速度却不确定,有时候会快一些,有时候会特别慢。它通过判断水池里水的状态,就可以自由控制进水的速度。
或者再想象一下包饺子的过程,包馅的需要等着擀皮的。如果擀皮的每擀一个就交给包馅的,速度就会很慢;但如果中间放一个盆子,擀皮的只管往里扔,包馅的只管从盆里取,这个过程就快得多。许多工厂流水线也经常使用这种方法,可见“缓冲”这个理念的普及性和实用性。
从宏观上来说,JVM 的堆就是一个大的缓冲区,代码不停地在堆空间中生产对象,而垃圾回收器进程则在背后默默地进行垃圾回收。通过上述比喻和释意,你可以发现缓冲区的好处:
-
缓冲双方能各自保持自己的操作节奏,操作处理顺序也不会打乱,可以 one by one 顺序进行;
-
以批量的方式处理,减少网络交互和繁重的 I/O 操作,从而减少性能损耗;
-
优化用户体验,比如常见的音频/视频缓冲加载,通过提前缓冲数据,达到流畅的播放效果。
Java 语言广泛应用了缓冲,在 IDEA 中搜索 Buffer,可以看到长长的类列表,其中最典型的就是文件读取和写入字符流。
文件读写流
接下来,本文将以文件读取和写入字符流为例进行讲解。
Java 的 I/O 流设计,采用的是装饰器模式,当需要给类添加新的功能时,就可以将被装饰者通过参数传递到装饰者,封装成新的功能方法。下图是装饰器模式的典型示意图,就增加功能来说,装饰模式比生成子类更为灵活。
在读取和写入流的 API 中,BufferedInputStream 和 BufferedReader 可以加快读取字符的速度,BufferedOutputStream 和 BufferedWriter 可以加快写入的速度。下面是直接读取文件的代码实现:
int result = 0; try(Reader reader=new FileReader(FILE_PATH)){ int value; while((value=reader.read())!=-1){ result+=value; } } return result;
要使用缓冲方式读取,只需要将 FileReader 装饰一下即可:
int result = 0; try (Reader reader = new BufferedReader(new FileReader(FILE_PATH))) { int value; while ((value = reader.read()) != -1) { result += value; } } return result;
我们先看一下与之类似的,BufferedInputStream 类的具体实现方法:
//代码来自JDK public synchronized int read () throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; }
当缓冲区的内容读取完毕,将尝试使用 fill 函数把输入流读入缓冲区:
//代码来自JDK private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ if (markpos > 0) { /* can throw away early part of the buffer */ int sz = pos - markpos; System.arraycopy(buffer, markpos, buffer, 0, sz); pos = sz; markpos = 0; } else if (buffer.length >= marklimit) { markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ } else if (buffer.length >= MAX_BUFFER_SIZE) { throw new OutOfMemoryError("Required array size too large"); } else { /* grow buffer */ int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? pos * 2 : MAX_BUFFER_SIZE; if (nsz > marklimit) nsz = marklimit; byte[] nbuf = new byte[nsz]; System.arraycopy(buffer, 0, nbuf, 0, pos); if (!U.compareAndSetObject(this, BUF_OFFSET, buffer, nbuf)) { // Can't replace buf if there was an async close. // Note: This would need to be changed if fill() // is ever made accessible to multiple threads. // But for now, the only way CAS can fail is via close. // assert buf == null; throw new IOException("Stream closed"); } buffer = nbuf; } count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; }
程序会调整一些读取的位置,并对缓冲区进行位置更新,然后使用被装饰的 InputStream 进行数据读取:
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
那么为什么要这么做呢?直接读写不行吗?
这是因为:字符流操作的对象,一般是文件或者 Socket,要从这些缓慢的设备中,通过频繁的交互获取数据,效率非常慢;而缓冲区的数据是保存在内存中的,能够显著地提升读写速度。
既然好处那么多,为什么不把所有的数据全部读到缓冲区呢?
这就是一个权衡的问题,缓冲区开得太大,会增加单次读写的时间,同时内存价格很高,不能无限制使用,缓冲流的默认缓冲区大小是 8192 字节,也就是 8KB,算是一个比较折中的值。
这好比搬砖,如果一块一块搬,时间便都耗费在往返路上了;但若给你一个小推车,往返的次数便会大大降低,效率自然会有所提升。
下图是使用 FileReader 和 BufferedReader 读取文件的 JMH 对比,可以看到,使用了缓冲,读取效率有了很大的提升(暂未考虑系统文件缓存)。
package cn.wja; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.io.BufferedReader; import java.io.FileReader; import java.io.Reader; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(1) public class BenchmarkReader { private static final String FILE_PATH = "F:\\农民工老王\\servertools.zip"; @Benchmark public int bufferedReaderTest() throws Exception { int result = 0; try (Reader reader = new BufferedReader(new FileReader(FILE_PATH))) { int value; while ((value = reader.read()) != -1) { result += value; } } return result; } @Benchmark public int fileReadTest() throws Exception { int result = 0; try (Reader reader = new FileReader(FILE_PATH)) { int value; while ((value = reader.read()) != -1) { result += value; } } return result; } public static void main(String[] args) throws Exception { Options opts = new OptionsBuilder().include(BenchmarkReader.class.getSimpleName()).build(); new Runner(opts).run(); } }
日志缓冲
日志是程序员们最常打交道的地方。在高并发应用中,即使对日志进行了采样,日志数量依旧惊人,所以选择高速的日志组件至关重要。
SLF4J 是 Java 里标准的日志记录库,它是一个允许你使用任何 Java 日志记录库的抽象适配层,最常用的实现是 Logback,支持修改后自动 reload,它比 Java 自带的 JUL 还要流行。
Logback 性能也很高,其中一个原因就是异步日志,它在记录日志时,使用了一个缓冲队列,当缓冲的内容达到一定的阈值时,才会把缓冲区的内容写到文件里。使用异步日志有两个考虑:-
同步日志的写入,会阻塞业务,导致服务接口的耗时增加;
-
日志写入磁盘的代价是昂贵的,如果每产生一条日志就写入一次,CPU 会花很多时间在磁盘 I/O 上。
Logback 的异步日志也比较好配置,我们需要在正常配置的基础上,包装一层异步输出的逻辑
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_HOME" value="."/> <property name="ENCODER_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%10.10thread] [%X{X-B3-TraceId}] %logger{20} - %msg%n"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/test.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/backup/log.log.%d{yyyy-MM-dd}</fileNamePattern> <maxHistory>100</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${ENCODER_PATTERN}</pattern> </encoder> </appender> <appender name="FILE2" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/test2.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/backup/log2.log.%d{yyyy-MM-dd}</fileNamePattern> <maxHistory>100</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${ENCODER_PATTERN}</pattern> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>512</queueSize> <appender-ref ref="FILE"/> </appender> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${ENCODER_PATTERN}</pattern> </layout> </appender> <logger name="cn.wja.log.synclog" level="INFO"> <appender-ref ref="ASYNC"/> </logger> <logger name="cn.wja.log.asynclog" level="INFO"> <appender-ref ref="FILE2"/> </logger> </configuration>
如下图,异步日志输出之后,日志信息将暂存在 ArrayBlockingQueue 列表中,后台会有一个 Worker 线程不断地获取缓冲区内容,然后写入磁盘中。
上图中有三个关键参数:-
queueSize,代表了队列的大小,默认是256。如果这个值设置的太大,大日志量下突然断电,会丢掉缓冲区的内容;
-
maxFlushTime,关闭日志上下文后,继续执行写任务的时间,这是通过调用 Thread 类的 join 方法来实现的(worker.join(maxFlushTime));
-
discardingThreshold,当 queueSize 快达到上限时,可以通过配置,丢弃一些级别比较低的日志,这个值默认是队列长度的 80%;但若你担心可能会丢失业务日志,则可以将这个值设置成 0,表示所有的日志都要打印。
缓冲区优化思路
毫无疑问缓冲区是可以提高性能的,但它通常会引入一个异步的问题,使得编程模型变复杂。
通过文件读写流和 Logback 两个例子,我们来看一下对于缓冲区设计的一些常规操作。
如下图所示,资源 A 读取或写入一些操作到资源 B,这本是一个正常的操作流程,但由于中间插入了一个额外的存储层,所以这个流程被生生截断了,这时就需要你手动处理被截断两方的资源协调问题。
根据资源的不同,对正常业务进行截断后的操作,分为同步操作和异步操作。同步操作
同步操作的编程模型相对简单,在一个线程中就可完成,你只需要控制缓冲区的大小,并把握处理的时机。比如,缓冲区大小达到阈值,或者缓冲区的元素在缓冲区的停留时间超时,这时就会触发批量操作。
由于所有的操作又都在单线程,或者同步方法块中完成,再加上资源 B 的处理能力有限,那么很多操作就会阻塞并等待在调用线程上。比如写文件时,需要等待前面的数据写入完毕,才能处理后面的请求。
异步操作
异步操作就复杂很多。
缓冲区的生产者一般是同步调用,但也可以采用异步方式进行填充,一旦采用异步操作,就涉及缓冲区满了以后,生产者的一些响应策略。
此时,应该将这些策略抽象出来,根据业务的属性选择,比如直接抛弃、抛出异常,或者直接在用户的线程进行等待。你会发现它与线程池的饱和策略是类似的,这部分的详细概念将在后续的文章中讲解。
许多应用系统还会有更复杂的策略,比如在用户线程等待,设置一个超时时间,以及成功进入缓冲区之后的回调函数等。
对缓冲区的消费,一般采用开启线程的方式,如果有多个线程消费缓冲区,还会存在信息同步和顺序问题。
Kafka缓冲区示例
这里以一个常见的面试题来讲解上面的知识点:Kafka 的生产者,有可能会丢数据吗?
如图,要想解答这个问题,需要先了解 Kafka 对生产者的一些封装,其中有一个对性能影响非常大的点,就是缓冲。生产者会把发送到同一个 partition 的多条消息,封装在一个 batch(缓冲区)中。当 batch 满了(参数 batch.size),或者消息达到了超时时间(参数 linger.ms),缓冲区中的消息就会被发送到 broker 上。
这个缓冲区默认是 16KB,如果生产者的业务突然断电,这 16KB 数据是没有机会发送出去的。此时,就造成了消息丢失。
解决的办法有两种:
- 把缓冲区设置得非常小,此时消息会退化成单条发送,这会严重影响性能;
- 消息发送前记录一条日志,消息发送成功后,通过回调再记录一条日志,通过扫描生成的日志,就可以判断哪些消息丢失了。
另外一个面试的问题是:Kafka 生产者会影响业务的高可用吗?
这同样和生产者的缓冲区有关。缓冲区大小毕竟是有限制的,如果消息产生得过快,或者生产者与 broker 节点之间有网络问题,缓冲区就会一直处于 full 的状态。此时,有新的消息到达,会如何处理呢?
通过配置生产者的超时参数和重试次数,可以让新的消息一直阻塞在业务方。一般来说,这个超时值设置成 1 秒就已经够大了,有的应用在线上把超时参数配置得非常大,比如 1 分钟,就造成了用户的线程迅速占满,整个业务不能再接受新的请求。
缓冲区的其他案例
使用缓冲区来提升性能的做法非常多,下面再举几个例子:
-
StringBuilder 和 StringBuffer,通过将要处理的字符串缓冲起来,最后完成拼接,提高字符串拼接的性能;
-
操作系统在写入磁盘,或者网络 I/O 时,会开启特定的缓冲区,来提升信息流转的效率。通常可使用 flush 函数强制刷新数据,比如通过调整 Socket 的参数 SO_SNDBUF 和 SO_RCVBUF 提高网络传输性能;
-
MySQL 的 InnoDB 引擎,通过配置合理的 innodb_buffer_pool_size,减少换页,增加数据库的性能;
-
在一些比较底层的工具中,也会变相地用到缓冲。比如常见的 ID 生成器,使用方通过缓冲一部分 ID 段,就可以避免频繁、耗时的交互。
缓冲区的注意事项
虽然缓冲区可以帮我们大大地提高应用程序的性能,但同时它也有不少问题,在我们设计时,要注意这些异常情况。
其中,比较严重就是缓冲区内容的丢失。即使你使用 addShutdownHook 做了优雅关闭,有些情形依旧难以防范避免,比如机器突然间断电,应用程序进程突然死亡等。这时,缓冲区内未处理完的信息便会丢失,尤其金融信息,电商订单信息的丢失都是比较严重的。
所以,内容写入缓冲区之前,需要先预写日志,故障后重启时,就会根据这些日志进行数据恢复。在数据库领域,文件缓冲的场景非常多,一般都是采用 WAL 日志(Write-Ahead Logging)解决。对数据完整性比较严格的系统,甚至会通过电池或者 UPS 来保证缓冲区的落地。这就是性能优化带来的新问题,必须要解决。
小结
可以看到,缓冲区优化是对正常的业务流程进行截断,然后加入缓冲组件的一个操作,它分为同步和异步方式,其中异步方式的实现难度相对更高。
大多数组件,从操作系统到数据库,从 Java 的 API 到一些中间件,都可以通过设置一些参数,来控制缓冲区大小,从而取得较大的性能提升。但需要注意的是,某些极端场景(断电、异常退出、kill -9等)可能会造成数据丢失,若你的业务对此容忍度较低,那么你需要花更多精力来应对这些异常。
在我们面试的时候,除了考察大家对知识细节的掌握程度,还会考察总结能力,以及遇到相似问题的分析能力。大家在平常的工作中,也要多多总结,多多思考,窥一斑而知全貌。如此回答,必会让面试官眼前一亮。
如需转载,请注明本文的出处:农民工老王的CSDN博客https://blog.csdn.net/monarch91 。
-
-
环形缓冲区实现类(RingBuffer)
2015-09-01 12:49:27环形缓冲区实现类(RingBuffer) -
基于未确知测度理论的CCPM缓冲区尺寸设计
2020-06-16 23:05:15引入未确知测度理论,计算工序特性对缓冲区尺寸影响程度的综合测度向量,利用工序特性对缓冲区尺寸的中等影响程度进行定性判断,并进一步采用置信度识别准则定量确定缓冲区尺寸。算例结果表明,工序A~Z的缓冲区影响因子... -
队列1-环形缓冲区
2019-01-27 16:58:34本篇为队列的第一篇文章,介绍基于数组结构的一个环形缓冲区队列。我觉得没有必要再从数组来写起,毕竟对于数组本身来说,我觉得是没有太多可说的,但是基于数组的数据结构就有的说了。 什么是环形缓冲区 环形缓冲...本篇为队列的第一篇文章,介绍基于数组结构的一个环形缓冲区队列。我觉得没有必要再从数组来写起,毕竟对于数组本身来说,我觉得是没有太多可说的,但是基于数组的数据结构就有的说了。
什么是环形缓冲区
- 环形缓冲区,顾名思义就是一个环状的存储数据的区域,其空间使用数组进行构造(链表也可以)。环形缓冲区特点是读和写可以是分开的,写入数据之后可以先不去读取,等到需要读取的时候再去读取,并且数据一经读取之后就做丢弃处理(当然也可以实现重复读取的效果,不过大多用作一次性读取),等于说是一次性的读取。
- 假设一个长度为256字节的数组,构建出一个环形缓冲区,当写操作进行到数组的第256项之后,再一次写入就会回到第0个进行写入;同样读操作是读取到数组的第256项时,再一次进行读取就会回到数组的第一项。是谓环形缓冲
可以看到,环形缓冲区是一种先进先出的队列类型结构,通常情况下会用于一个符合生产着消费者模型的场景。比如说视频帧数据的管理、消息队列的管理等等。
队列类型的数据结构还有链表形式,只不过对于环形缓冲区来说,使用数组更加的高效。本文就基于 Linux 内核里面的 kfifo 队列实现一个高效、自定义功能并且以面向对象模式组织的环形缓冲区模块,不是照抄,理会精髓,自己实现,然后加入一些扩展。
基本结构
一个环形缓冲区包括以下元素:
- 读指针、写指针。
- 数组类型的存储空间(有链表类型的,但是这里只说数组类型)。
- 缓冲区是否满标志、锁。
用图形化的描述就如下图所示:
上面一个是它的直观存储方式,也就是一个数组类型的结构,其中
r
代表读指针,用于指示下一个应该读取的环形缓冲区元素。w
代表写指针,用于指示下一个该写入的位置。黑色的一列小方框就是用于存储环形缓冲区数据的存储空间。锁在单生产者、单消费者的情况下是不需要的,原因后面讲。其中r
,w
指针其实很容易看出来,它就是个数组的下标索引。下面一个圆形的图就是形象化的环形缓冲区,其实把直线棍状的数组给它折叠起来了而已,看起来没头没尾的一个自交型贪吃蛇结构。这个图更接近环形缓冲区的本身的抽象化形象,在代码实现上其实就是把读写指针取一个模完成"环形化"。
这里要提一下的是关于缓冲区的满与空的标志,首先说结论:
- 当
r
与w
相等的时候,就说明这个环形缓冲区队列空了。 - 当
r
== (w
+ 1)%size
的时候就说明这个环形缓冲区满了。
上面两个断言似乎有冲突的地方,比如写指针绕过一圈子之后如果
w
与r
相等也能够说这个环形缓冲区是满的,并且上面第二条看起来会空余一个元素没有用到啊。第一个我们在代码实现的时候就会在出队列的时候才会判断这个缓冲区是否为空,第二个为了方便起见,环形缓冲区始终会空出一个元素的位置来明确的区分队列空与队列满的标记。所以第一个冲突的情况就不会发生。如果希望能够充分利用存储空间的话就需要一个额外的变量来存储目前缓冲区里面已经存放好的元素有多少个,然后与环形缓冲区创建之初指定的元素总数进行比较,这样就可以充分利用所有缓冲区里面的存储空间,因为大多数时候环形缓冲区的元素都是大于一个字节的。
代码实现
保留之前的习惯,在文章里面会尽可能少的贴代码,我觉得贴的代码过多会导致整篇文章很难看,并且使得文章显得冗长并且有用的部分还不多,代码我贴到 github 上面在文末给出链接。
首先我这里的代码实现提出了几个要求:
- 能够自定义环形缓冲区的元素个数、自定义每个元素的字节数。
- 能够支持多线程安全的方式进行读取或者写入。
- 模块化,使用者只需要初始化一个环形缓冲区对象,然后可以很方便地使用它。
- 可以很方便、快速地初始化多个环形缓冲区实例。
这里我会先规定一个环形缓冲区的抽象化结构体,在需要使用的时候就实例化一个环形缓冲区结构体,我把它的结构写成下面这种:
struct ring_buffer { int (*rb_in)(struct ring_buffer *rb_hd, void *buf_in, uint32_t elem_num); int (*rb_out)(struct ring_buffer *rb_hd, void *buf_out, uint32_t elem_num); void (*rb_reset)(struct ring_buffer *rb_hd); uint32_t (*rb_g_bufsize)(struct ring_buffer *rb_hd); uint32_t (*rb_g_validnum)(struct ring_buffer *rb_hd); uint32_t (*rb_g_elemsize)(struct ring_buffer *rb_hd); void *private; };
里面的前面大部分项都不去解释了,很容易可以知道它们每个值的意义。最后一个
private
需要特别解释下,这个是用于模块内部的自用结构的索引。仔细看一下上面的结构体里面缺少了哪些元素?可以看到少了环形缓冲区的总大小、元素大小、锁、读指针、写指针等等。而这些东西对于使用者来讲是不需要用到的数据,不需要关心,而这部分数据我就放在另外一个内部的结构体里面了,它的定义如下所示:
struct ring_buffer_entity { pthread_mutex_t rb_lock; uint32_t rb_in; uint32_t rb_out; uint32_t elem_size; uint32_t elem_cnt; bool rb_full; unsigned char *rb_buf; struct ring_buffer_attr rb_attr; struct ring_buffer rb_handle; };
在使用的时候我通常会做以下的转换:
struct ring_buffer_entity *entity = (struct ring_buffer_entity *)ring_buffer->private;
。这样就达到了封装的目的,在 C++ 里面封装是不需要用得到那个private
的,但是在 C 里面就不得不用这种方式实现封装的目的。扯多了,回到代码中与环形缓冲区相关的地方。
临界判断
在上面的第二个结构体里面,读写指针都是32位的无符号整形,这个是有特殊作用的,因为这种情况下可以直接使用
rb_in - rb_out
来表示目前环形缓冲区里面有效的数据个数,不用取模,在写入之后rb_in
尽管加上写入元素的个数即可,也不用在写入结束的时候把rb_in
取模。想象一下无符号整形数的特点,就是在溢出的时候会恢复到0值,也就是 0xFFFFFFFF+1 会等于 0,在没有溢出的情况下
rb_in - rb_out
用于表示目前已写入的元素个数很好理解,那么一旦当rb_in
溢出了,rb_in - rb_out
还是可以满足计算要求。用一个实例套入计算即可,比如说现在环形缓冲区里面有三个元素,正常情况下
rb_in
与rb_out
的关系是类似 3与0,116与113 的关系,直接减去没有问题,但是如果这个时候rb_in
已经超了,比如此时rb_out == 0xFFFFFFFE
, 呢么rb_in
就是 0xFFFFFFFE+3,这个值在无符号的时候是2,因为溢出了,那么无符号的 2-0xFFFFFFFE 在内部计算的时候就是一个很大的负数,而这个负数重新转化为无符号类型就是 3.目前代码里面没有只用了一个已写入元素的个数计数和整个环形缓冲区的可存储元素总数来进行比较,没有使用
r
,w
指针本身来进行判断,这样会充分利用环形缓冲区里面的每一个存储空间。读写的实现
写入的核心代码有下面几个步骤:
uint32_t cp_cnt = min(要写入的元素个数, 剩余的元素个数); uint32_t cp_step1 = min(cp_cnt, 数组右侧剩余的可存储元素空间个数); memcpy(写指针在的位置, 输入buffer地址, cp_step1乘以元素的大小); memcpy(数组起始地址, 输入buffer剩下的数据起始地址, (cp_cnt-cp_step1)乘以元素大小); rb_ent->rb_in += cp_cnt; /* 写指针后移 */
读取的核心代码有下面几个步骤:
uint32_t cp_cnt = min(要读出的元素个数, 有效元素个数); uint32_t cp_step1 = min(cp_cnt, 数组右侧剩余的有效元素个数); if (NULL == buf_out) goto copy_end; memcpy(输出buffer地址, 读指针在的位置, cp_step1乘以元素大小); memcpy(输出buffer剩余空间起始地址, 数组零下标起始地址, (cp_cnt-cp_step1)乘以元素大小); copy_end: rb_ent->rb_out += cp_cnt; /* 读指针后移 */
这里读写指针不必每次后移的时候都取模,只用在索引数组下标的时候对其取模即可,原因在上一条里面描述过了。
如果在单生产者单消费者的情况下,这个读写的过程是不用加锁的,唯一需要担心的也就是指令重排了,但是这种情况发生的概率也是极小的,一般情况下在嵌入式的场景里面基本是不用担心的。
那么如果在写的时候被打断,看下会发生什么情况,由于写过程中用到的会时刻变化的共享变量也就是
rb_out
了,如果在取到了rb_out
的值之后它的值被别人改变了,也就是环形缓冲区中的存储空间又被释放出了一部分,此时顶多会导致本来可以写入的部分由于缓冲区被判定为满而写不进去了,稍等片刻再写或者干脆丢掉也不影响,整体上不会导致读写错乱。而读的过程也是类似,顶多是有些已经写入的东西被误判为还没有写入,那下次再去读取就好,无非是多耗费了一点时间,况且加锁的话这部分时间也是无法省去的。这也是代码里面为什么要在数据拷贝完成之后在改变
rb_in
与rb_out
的一个考虑,因为如果在拷贝之前改变它的值就有可能读出来非法的值或者写入值把原来的值给覆盖了。所以单生产者但消费者的情况下,基本上是不用考虑锁的问题的。从另一种角度来讲,这种队列模式其实不太可能用于多个消费者的情况,原因是因为通常情况下消费者是不能够错过队列中的任何一个消息的,或者说必须获取连续的队列内容。
想象一下多消费者的实现,我这里有一种思路:提前确定好消费者的数量,然后为每一个队列项添加一个引用计数,一旦有一个消费者取用就将引用计数减一,到0才真正从队列里面删掉这个数据。这样会有几个问题:
- 一旦有任何一个消费者阻塞,其它的都会阻塞。
- 必须确定每一个元素的引用计数,需要添加一个成员,这样会导致一次性的多个元素拷贝变得很困难,因为可能有的读得多,有的读的少,这样会导致引用计数无法保持一致性的减少。
所以,多个消费者一般是不会使用同一个队列对象的,多个生产者却是可能的,因为生产元素有很多时候无需满足十分有序的输入,比如命令分发、消息分发队列,这个时候可以只在生产者那一端也就是队列写入操作那里加上锁,读出就不需要加锁了。
代码设计
在代码里面我添加了一些属性,比如线程安全属性,与普通情况不同的是它加了一把锁,但是表现在使用者那里就对应的是同一个回调函数成员,只不过其指向的函数实现不一样而已。
代码采用了面向对象的方式进行编写,可以非常方便的初始化一个环形缓冲区,并且使用实例化对象结构体内部的成员就可以完成整个的环形缓冲区的操作,十分方便。
代码参考了内核里面的 kfifo 的实现,力求尽量地精简,但是为了使用的便捷,加入了不少的自定义内容,并且加入了一些可能会用得到的特性,比如线程安全属性等等。
环形缓冲区内部不区分你想存入的数据结构类型,它只管按照当初约定好的元素长度以及你传递给它的读写 buffer 地址来进行指定长度的拷贝或者读取,数据类型的一致性要靠使用者自己来保证。
利用 void* 指针的特性来屏蔽一些用户不需要的细节,比如上面说到的两个结构体,一个作为模块内部使用,一个作为用户与模块内部交互的接口使用。
End
这是队列的第一篇,主要介绍下环形缓冲区这个队列,下一篇文章会介绍一下链表类型的队列,会先写一下链表队列的实现,然后再结合一个实际的链表类型的应用进行辅助,风格与这个类似,力求使用方便,代码清晰易懂。
需要注意的是代码里面肯定会不可避免的有一些 bug,要实现一个无 bug 的小模块显然比我想象当中的更困难,这一点在工作当中已经无数次验证过,所以当你使用我的代码遇到一些操蛋的问题,那一定不是用法的问题,我觉得大概率是我的代码 bug。那么为什么会有 bug 呢,主要还是我没有精力与动力去搞大规模测试,代码精确 review 这些,领会精髓吧,如果后续有必要,比如有人提了 issue 或者啥的我可能才会去修一修,不然凭我自己的主观能动性怕是比较玄学了。
这篇其实是比较浅显易懂的,不过不要怪我水,因为写一篇技术类的文章太难了,要有代码要有文章,要有调试要尽量少错误,由浅及深,后续估计进度会越来越慢的(逃。
Github 代码链接:链接
想做的事情就去做吧 -
ringbuff | 通用FIFO环形缓冲区实现库
2020-06-06 10:58:39不会单纯的介绍分享项目,还会包含作者亲自实践的过程分享,甚至还会有对它背后的设计思想解读。 目前本专栏包含的开源项目有: cJSON | 一个轻量级C语言JSON解析器 paho | 支持10种语言编写mqtt客户端,总有一款... -
多图详解缓冲区溢出问题
2020-11-18 23:43:21缓冲区溢出一个常见的后果是:黑客利用函数调用过程中程序的返回地址,将存放这块地址的指针精准指向计算机中存放攻击代码的位置,造成程序异常中止。为了防止发生严重的后果,计算机会采用栈随机化,利用金丝雀值... -
缓冲区溢出漏洞浅析
2019-10-01 17:41:41缓冲区是指内存中一段连续的地址空间,用来缓存数据。在大多数开发语言中,把数组和指针分配的空间作为缓冲区。缓冲区溢出是指读取或写入的范围超过数组或指针指向的缓冲区空间,导致程序运行期间发生异常。缓冲区... -
QT QSerialPort 封装,阻塞方式发送数据后等待读取缓冲区数据结果,使用QT信号量阻塞,线程中初始化串口和...
2018-05-23 09:01:12QT QSerialPort 封装,阻塞方式发送数据后等待读取缓冲区数据结果,使用QT信号量阻塞,线程中初始化串口和读取数据。 -
环形缓冲器
2019-06-02 21:02:38环形缓冲器(ringr buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),圆形缓冲区(circula buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。... -
嵌入式中 gui 显示单缓冲、双缓冲、三缓冲的原理
2019-07-23 10:33:04gui 中的显示功能 gui 中的显示最终通过调用...对与 gui 而言,所有的在调用 lcd 驱动刷新 framebuffer 到屏幕上显示之前都是通过对 framebuffer 的操作完成。这里提及的操作主要使用 memcpy、memset 来完成,这也... -
tcp socket的发送与接收缓冲区
2021-01-12 11:23:02tcp socket的发送缓冲区实际上是一个结构体struct sk_buff的队列,我们可以把它称为发送缓冲队列,由结构体struct sock的成员sk_write_queue(struct sk_buf_head)表示。sk_write_queue是一个结构体struct sk_buff_... -
应用控制的Web服务器磁盘缓冲方法 (2007年)
2021-04-22 16:20:55这样,服务器可在用户空间控制文件缓冲,从而准确判断文件是否在缓冲之中,并依此来调度请求,以提高处理器和磁盘的I/O并行度。同时,服务器可采用适应自身特点的缓冲和预读策略,以提高缓冲的命中率。作为示例,将... -
GeoTools jts java 构建线的缓冲区 判断点在面内
2019-03-27 10:11:02//点是否在多边形内判断 Coordinate point = new Coordinate(116.663609,40.387187); PointLocator a=new PointLocator(); boolean p1=a.intersects(point, bg); if(p1) System.out.println("point1:"+"该点在... -
STM32进阶之串口环形缓冲区实现
2018-06-04 10:03:13如需转载请说明出处:STM32进阶之串口环形缓冲区实现 队列的概念 在此之前,我们来回顾一下队列的基本概念: 队列 (Queue):是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在一端插入(入队),... -
InnoDB学习笔记二缓冲池(buffer pool)
2022-05-09 17:35:34本篇是mysql的InnoDB引擎学习笔记之二缓冲池,主要介绍了缓冲池的概念以及如何调整其大小,接着介绍了其内存结构 如 free list、flush list、lru list的结构、实现方式使用场景。希望对学习innodb的同学有所帮助 -
环形缓冲区的实现
2019-05-18 20:55:37TRUE:写入成功 5* @author 杰杰 6* @date 2018 7* @version v1.0 8* @note 往环形缓冲区写入u8类型的数据 9*/10u8 Write_RingBuff(u8 data)11{12 if(ringBuff.Lenght >= RINGBUFF_LEN) //判断缓冲区是否已满13 {14 ... -
21 Redis 缓冲区的用法
2021-12-09 22:11:3921 Redis 缓冲区的用法前言一、客户端输入和输出缓冲区二、输入缓冲区溢出的应对方法三、输出缓冲区溢出的应对方法四、主从集群中的缓冲区总结 前言 缓冲区的功能是用一块内存空间来暂时存放命令数据,以免出现... -
清理缓冲区的方法
2021-03-09 02:56:391. 理解一下输入输出缓冲区的概念以一个例子说明,比如我想把一篇文章以字符序列的方式输出到计算机显示器屏幕上,那么我的程序内存作为数据源而显示器驱动程序作为数据目标,如果数据源直接对数据目标发送数据的话... -
C++缓冲区的理解
2018-03-05 17:23:32原文链接下面介绍缓冲区的知识。一、什么是缓冲区缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。... -
UDP写缓冲区(发送缓冲区)分析
2020-06-09 10:04:49最近,碰到UDP是否有写缓冲区的疑问,对于应用,如下图linux手册中有设置UDP发送缓冲区相关属性,也明确提到了send buffer的概念: 那这是否意味着UDP是有发送缓冲区的吗?我们再看一下《UNIX Network Programming... -
I/O流(5) 利用缓冲流实现对文件的复制操作
2019-11-24 14:47:28前面的文章已经介绍过关于文件的读写操作,这篇文章介绍利用缓冲流实现文件的读写操作。 -
NIO之缓冲区【基础内容】
2019-04-10 12:30:58缓冲区Buffer 1.缓冲区介绍 一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。缓冲区可以写满和释放。对于每个非布尔原始数据类型都有一个缓冲... -
缓冲区溢出攻击
2021-02-06 20:32:00缓冲区溢出(buffer-overflow)是一种非常普遍、同时非常危险的漏洞,在...本文将主要介绍堆栈溢出攻击,并实现对一个ubuntu 16.04系统的简单的栈攻击,获取其root权限。 实验平台 操作系统:SEED Ubuntu16.04 VM (32-bit -
环形缓冲区
2017-03-18 16:37:29在嵌入式软件开发中,经常会遇到这样的场景,创建一个临时缓冲区用于存放待读取的数据。这时候,环形缓冲区是一个不错的选择。所谓环形缓冲区就是一段有限的内存空间,并且有两个指针分别代表读、写指向这一块内存,...