自从项目用了 Disruptor,性能提升了 2.5 倍!

简介: 自从项目用了 Disruptor,性能提升了 2.5 倍!

一、CPU Cache

存储设备往往是速度越快价格越昂贵,速度越快价格越低廉。


在计算机中,CPU 的速度远高于主存的速度,而主存的速度又远高于磁盘的速度。为了解决不同存储部件的速度不对等问题,让高速设备充分发挥性能,引入了多级缓存机制。


为了解决内存和 CPU 的速度不匹配问题,相继引入了 L1 Cache、L2 Cache、L3 Cache,数字越小,容量越小,速度越快,位置越接近 CPU。image.png

现在的 CPU 都是由多个处理器,每个处理器由多个核心构成。 一个处理器对应一个物理插槽,不同的处理器间通过 QPI 总线相连。一个处理器间的多核共享 L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构:image.png二、缓存行与伪共享

缓存中的数据并不是独立的进行存储的,它的最小存储单位是缓存行,缓存行的大小是2的整数幂个字节,最常见的缓存行大小是 64 字节。CPU 为了执行的高效,会在读取某个对象时,从内存上加载 64 的整数倍的长度,来补齐缓存行。


以 Java 的 long 类型为例,它是 8 个字节,假设我们存在一个长度为 8 的 long 数组 arr,那么CPU 在读取 arr[0] 时,首先查询缓存,缓存没有命中,缓存就会去内存中加载。由于缓存的最小存储单位是缓存行,64 字节,且数组的内存地址是连续的,则将 arr[0] 到 arr[7] 加载到缓存中。后续 CPU 查询 arr[6] 时候也可以直接命中缓存。

image.png现在假设多线程情况下,线程 A 的执行者 CPU Core-1 读取 arr[1],首先查询缓存,缓存没有命中,缓存就会去内存中加载。


从内存中读取 arr[1] 起的连续的 64 个字节地址到缓存中,组成缓存行。由于从arr[1] 起,arr 的长度不足够 64 个字节,只够 56 个字节。假设最后 8 个字节内存地址上存储的是对象 bar,那么对象 bar 也会被一起加载到缓存行中。


image.png现在有另一个线程 B,线程 B 的执行者 CPU Core-2 去读取对象 bar,首先查询缓存,发现命中了,因为 Core-1 在读取 arr 数组的时候也顺带着把 bar 加载到了缓存中。


这就是缓存行共享,听起来不错,但是一旦牵扯到了写入操作就不妙了。


假设 Core-1 想要更新 arr[7] 的值,根据 CPU 的 MESI 协议,那么它所属的缓存行就会被标记为失效。因为它需要告诉其他的 Core,这个 arr[7] 的值已经被更新了,缓存已经不再准确了,你必须得重新去内存拉取。但是由于缓存的最小单元是缓存行,因此只能把 arr[7] 所在的一整行给标识为失效。


此时 Core-2 就会很郁闷了,刚刚还能够从缓存中读取到对象 bar,现在再读取却被告知缓存行失效,必须得去内存重新拉取,延缓了 Core-2 的执行效率。


这就是缓存伪共享问题,两个毫无关联的线程执行,一个线程却因为另一个线程的操作,导致缓存失效。这两个线程其实就是对同一缓存行产生了竞争,降低了并发性。


三、Disruptor 缓存行填充

Disruptor 为了解决伪共享问题,使用的方法是缓存行填充。这是一种以空间换时间的策略,主要思想就是通过往对象中填充无意义的变量,来保证整个对象独占缓存行。


举个例子,以 Disruptor 中的 Sequence 为例,在 volatile long value 的前后各放置了 7 个 long 型变量,确保 value 独占一个缓存行。image.png如下图所示,其中 V 就是 Value 类的 value,P 为 value 前后填充的无意义 long 型变量,U 为其它无关的变量。不论什么情况下,都能保证 V 不和其他无关的变量处于同一缓存行中,这样 V 就不会被其他无关的变量所影响。

image.png这里的 V 也不限定为 long 类型,其实只要对象的大小大于等于8个字节,通过前后各填充 7 个 long 型变量,就一定能够保证独占缓存行。


此处以 Disruptor 的 RingBuffer 为例,最左边的 7 个 long 型变量被定义在顶级父类 RingBufferPad 中,最右边的 7 个 long 型变量被定义在 RingBuffer 的最后一行变量定义中,这样所有的需要独占的变量都被左右 long 型给包围,确保会独占缓存行。

image.png四、@Contended

在 JDK 1.8 中,提供了 @sun.misc.Contended 注解,使用该注解就可以让变量独占缓存行,不再需要手动填充了。 另外,关注公众号Java技术栈,在后台回复:Java,可以获取我整理的 Java 1.8 系列教程,非常齐全。


注意,JVM 需要添加参数 -XX:-RestrictContended 才能开启此功能。


如果该注解被定义在了类上,表示该类的每个变量都会独占缓存行;如果被定义在了变量上,通过指定 groupName,相同的 groupName 会独占同一缓存行。image.png@Contended 在 JDK 源码中已经有所应用,以 Thread 类为例,为了保证多线程情况下随机数的操作不会产生伪共享,相关的变量被设置为同一 groupName。image.png

五、速度测试

将 volatile long value 封装为对象,四线程并行,每个线程循环 1 亿次,对 value 进行更新操作,测试缓存行对速度的影响。

  • CPU:AMD 3600 3.6 GHz
  • Memory:16 GBimage.png作者: Jitwxs

链接: https://jitwxs.cn/13836b16.html


近期热文推荐:


1.Java 15 正式发布, 14 个新特性,刷新你的认知!!


2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!


3.我用 Java 8 写了一段逻辑,同事直呼看不懂,你试试看。。


4.吊打 Tomcat ,Undertow 性能很炸!!


5.《Java开发手册(嵩山版)》最新发布,速速下载!


觉得不错,别忘了随手点赞+转发哦!


image.png

相关文章
|
1月前
|
数据采集 Cloud Native Java
10 倍性能提升, GraalVM 应用可观测实践
本文介绍了 GraalVM 静态编译技术在云原生环境下的应用:ARMS 发布了支持 GraalVM 应用的 Java Agent 探针,可为 GraalVM 应用提供开箱即用的可观测能力。同时,文章还提供了使用 ARMS 对 GraalVM 应用进行可观测的详细步骤。
484 11
|
人工智能 NoSQL Redis
如何将分布式锁性能提升100倍【含面试题】
如何将分布式锁性能提升100倍
315 0
|
SQL 机器学习/深度学习 分布式计算
「大数据架构」Spark 3.0发布,重大变化,性能提升18倍
「大数据架构」Spark 3.0发布,重大变化,性能提升18倍
|
JSON 自然语言处理 Java
性能飙升20倍!!! 超高性能协议框架fury完爆protostuff
性能飙升20倍!!! 超高性能协议框架fury完爆protostuff
280 0
|
测试技术
10000次写1K 比 一次写10M 耗时多30倍
10000次写1K 比 一次写10M 耗时多30倍
|
SQL 测试技术 Apache
多场景下 3-11 倍性能提升,Apache Doris 1.2 新版本性能揭秘!
多场景下 3-11 倍性能提升,Apache Doris 1.2 新版本性能揭秘!
188 0
|
缓存 前端开发 Java
是什么让一段20行代码的性能提升了10倍
性能优化显而易见的好处是能够节约机器资源。如果一个有2000台服务器的应用,整体性能提升了10%,理论上来说,就相当于节省了200台的机器。除了节省机器资源外,性能好的应用相对于性能差的应用,在应对流量突增时更不容易达到机器的性能瓶颈,在同样流量场景下进行机器扩容时,也只需要更少的机器,从而能够更快的完成扩容、应急操作。所以,性能好的应用相对于性能差的应用在稳定性方面也更胜一筹。
是什么让一段20行代码的性能提升了10倍
EMQ
|
缓存 运维 Kubernetes
5.0 版本持续优化:ExProto 吞吐性能提升
九月,EMQX 5.0保持稳定更新,目前已发布5.0.8版本,企业版4.3&4.4发布最新维护版本。云服务方面,EMQX Cloud新增1000连接规格的专业版部署。
EMQ
265 0
5.0 版本持续优化:ExProto 吞吐性能提升
DHL
|
缓存 安全 算法
反射技巧让你的性能提升 N 倍
这个反射技巧可能让你的性能提升 N 倍,isAccessible 方法的作用,为什么将 Accessible 设置为 true 可以提升性能
DHL
228 0
反射技巧让你的性能提升 N 倍
|
运维 Java 数据库
JVM 调优一个月,性能提升了 400 倍!
通过这一个多月的努力,将 FullGC 从 40 次/天优化到近 10 天才触发一次,而且 YoungGC 的时间也减少了一半以上,这么大的优化,有必要记录一下中间的调优过程。
145 0
JVM 调优一个月,性能提升了 400 倍!