Java与CPU缓存的亲密接触之「伪共享」

Java:缓存行和伪共享 在Java中,缓存行(Cache Line)和伪共享(False Sharing)是与多线程访问共享数据相关的两个重要概念。 阅读详情

0?wx_fmt=jpeg

在解释【伪共享】这个概念之前,我们先来运行一段代码,小编的电脑上有4个core。

0?wx_fmt=jpeg

这个程序的逻辑是4个线程共享同一个数组读写不同下标的变量。每个线程循环1亿次读写,也就是+1操作。然后统计4个线程同时跑完总共花的时间。

0?wx_fmt=jpeg

下面我们来看看在小编的电脑上运行的结果

0?wx_fmt=jpeg

然后我把SharingLong里面的注释代码去掉,再跑了一下。

0?wx_fmt=jpeg

在性能上注释前后差别高达5比1,为什么会在性能上会产生如此大的差别呢?

这就是本篇要讲的主题【伪共享】,英文名叫False Sharing。而SharingLong里面的注释行一般称之为【缓存行填充】,英文名叫Cache Line Padding。

首先我们来计算一下SharingLong对象占用的内存空间,我们不考虑64位的情景,Java的对象都有一个2个word的头部,第一个word存储对象的hashcode和一些特殊的位标志,如GC的分代年龄、偏向锁标记等,第二个word存储对象的指针地址,一个word就是32位。然后加上v和6个p变量,总共就是8个long的长度,也就是64字节。

接下来我们要引入CPU缓存的概念。

0?wx_fmt=jpeg

现代的处理器一般都有3级缓存结构,L1、L2和L3,CPU直接访问主存是一个相对比较慢的操作,所以通过3级缓存来提升访存性能。我们将3个缓存当成一个整体来看待,它就是CPU缓存。缓存的制造成本非常昂贵,它一般要比主存空间小的多。

CPU在读主存的时候,会先将主存的一块数据加载到缓存上,然后在缓存上读取。当CPU写主存的时候,它会首先写缓存,在未来的某个时间点再一次性将缓存的数据全部刷回主存,这样就可以提高写操作的性能。因为计算机程序数据操作的局部性,CPU连续的指令倾向于访问相邻地址空间的数据,所以后续的读写操作有很大的概率可以直接在缓存上拿到数据。如果缓存上不存在,那就再去主存上加载进来。

缓存虽然小,但是也不是太小,CPU在加载主存数据时,如果一次性将整个Cache填满,但是接下来的指令访问的数据又不在缓存上,就会导致读浪费。另外如果只修改了其中几个字节的数据,但是得回写整个Cache到内存,这又会导致写浪费。

所以现代的CPU缓存一般是分行存储的,最小处理单位是一个行,这个行的长度一般来说就是上文提到的64字节,我们称之为【缓存行】。

0?wx_fmt=jpeg

SharingLong对象中v的值是volatile类型的,意味着CPU要保证v变量在不同线程之间的读写可见行。当CPU对v变量进行修改的时候会将数据立即回写至主存并将相应的缓存行置为失效。这样后续对v变量进行的读写操作都需要重新从内存中加载缓存行,这样就保证了其它线程读到的数据是最新的。

这点跟我们平常在Java基础教科书里提到的有点不一样。教科书里面为了便于新手理解,不会提及缓存,一般只会说volatile变量直接读写内存。

如果内存里有两个volatile变量在相邻的地址,两个cpu分别对v1和v2进行读和写操作,会发生什么情况呢?首先我们分解执行动作。图中的h表示对象头。

0?wx_fmt=jpeg

  1. CPU1对v1进行读操作,将内存里的v1加载到缓存行里。

  2. CPU2对v2进行读操作,将内存里的v2加载到缓存行里。

  3. CPU1对v1进行写操作,将缓存里的v1修改,然后回写到主存再将缓存行置为失效。

  4. CPU2对v2进行写操作,将缓存里的v2修改,然后回写到主存再将缓存行置为失效。

步骤1肯定先于步骤3,步骤2肯定先于步骤4。它们发生的顺序可能是 1->2->3->4 ,相当于两个CPU交叠运行,步骤1加载缓存行,步骤2发现数据就在缓存行里还是最新的,就省去了加载缓存行操作了,这时读操作做到了【共享】。紧接着步骤3正常进行写操作,然后步骤4来了,CPU2发现缓存行失效了,所以还得重新加载缓存行,然后再回写到主存再将缓存行置为失效。这里就发生了重复加载缓存行的现象,也即【写竞争】。如果不是volatile变量,步骤3的写操作是不会立即回写内存的,缓存行也就不会立即置为失效,这个时候步骤4来了CPU可以直接对缓存进行写操作,而不会出现浪费现象。我们称这种现象为【伪共享】,就是说这两个变量虽然共享同一个缓存行,但是它们之间会发生写竞争。

如果顺序是1->3->2->4,步骤1和步骤3的读操作这时就没能实现共享,还是会有浪费。

当系统的线程数越多时,写竞争越激烈,这种浪费就越多。

现在我们能明白为什么去掉注释后,程序会变慢,因为存在写竞争现象,数组中相邻的SharingLong.v共享了同一个缓存行。

那加上p1~p6这6个变量的意义是什么呢?我们看图。

0?wx_fmt=jpeg

我们发现加上6个long变量后,v1和v2将分别占用自己的缓存行,互不干扰,所以写竞争也就不存在了,效率自然就提升了。

不过缺点也是有的,就是缓存的利用率降低了,一个缓存行的空间才使用了1/4。这就是典型的空间换时间的场景。

例子中我们使用了volatile变量,那如果改成普通变量呢?我们运行一下,结果如下。

0?wx_fmt=jpeg

相当惊人,耗时上居然少了3个量级,这就是volatile在性能上的代价。普通变量不需要保证线程之间的读写的可见性,CPU对缓存修改后不需要立即回写内存,不存在写操作缓存穿透现象。而读操作也不需要总是重新从内存加载,那这个效率几乎完全就是缓存访问的效率,而对volatile变量的读写操作则接近内存访问的效率,差距自然如此明显。

你也许会问,知道这些有什么蛋用!

确是没什么蛋用,因为在现实世界,大部分操作都涉及到IO操作。根据水桶效应,其它环节优化到了极致,也无法提升整体的质量。

但是也不完全所有的应用都是IO操作型的,有一些场景下那是纯粹的内存操作。那么对于纯内存操作来说,理解【伪共享】知识可以帮你从性能上提升几倍甚至是几个数量级。

著名的disruptor框架正是使用了缓存行填充技术,才使得它的环形数组队列能如此高效。看wiki上的性能报告,disruptor的RingBuffer相比Java内置的ArrayBlockingQueue在OPS上高出近一个数量级,在队列延迟上则低了接近3个数量级。

0?wx_fmt=jpeg

阅读相关文章,请关注公众号【码洞】

ARMv7多核调试实战:寄存器位域操作与CodeWarrior配置详解 在嵌入式系统开发中,寄存器操作是底层硬件控制的基础技术。寄存器作为处理器与硬件交互的直接接口,其每个比特位都对应着特定的控制功能。传统上,开发者需要直接读写整个寄存器值,这种方式在精细控制时容易产生难以维护的“魔数”代码。寄存器位域技术通过将物理寄存器逻辑划分为多个有意义的字段,使开发者能够像操作结构体成员一样控制硬件,这大大提升了代码的可读性和可维护性。随着嵌入式系统复杂度的提升,多核处理器架构日益普及,多核调试技术成为开发高性能系统的关键。多核调试允许开发者同时监控多个处理核心的执行状态,分析核间通信与 阅读详情

相关推荐

Docker vs 原生安装:Neo4j不同部署方式的性能对比与选型建议

本文深入对比了Neo4j的Docker容器化部署与原生安装两种方式,通过详尽的性能测试数据,分析了它们在启动速度、查询性能、资源占用及I/O效率等方面的差异。文章结合不同应用场景(如开发测试、CI/CD、生产环境),提供了具体的配置调优建议与选型指南,帮助开发者根据团队技能、运维需求及性能目标做出明智的部署决策。

lambda的博客 404

JAVA中的伪共享与缓存行

JAVA中的伪共享与缓存行

qq_45443475的博客 1870

Ubuntu系统部署达梦DM8数据库:从内核参数到实例创建的完整指南

关系型数据库是企业数据管理的核心组件,其性能与稳定性直接取决于底层操作系统环境的优化配置。在Linux系统中,通过调整内核参数如文件句柄限制、共享内存大小和TCP连接设置,可以为数据库引擎提供高效的内存管理、进程通信和网络I/O支持,从而提升高并发场景下的处理能力。这些优化对于需要处理大量事务的OLTP系统尤为重要,能够显著减少资源竞争和延迟。以达梦数据库DM8在Ubuntu服务器上的部署为例,系统依赖包安装、专用用户权限规划以及关键内核参数的精细调整,共同构成了数据库稳定运行的基石。本文基于Ubuntu

weixin_34133829的博客 325

[C/C++] 32位系统和64位系统中各个变量类型占的字节数

32位编译器: char :1个字节 char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即4GB) short int : 2个字节 int: 4个字节 unsigned int : 4个字节 float: 4个字节 double: 8个字节 long: 4个字节 long long: 8个字节 unsigned long: 4个字...

ykun089的博客 5403

Java对象内存布局

一个Java对象在内存中包括三部分 对象头 实例数据 补齐填充

诺浅的专栏 1069

【信息科学与工程学】【数据科学】数据科学领域——第三篇 数学08 几何学07 网络几何01

编号类型领域问题问题的数学分析参数列表及参数的数值范围关联知识1组合优化 / 计算几何网络几何(几何图论):给定平面上的 n 个点(节点),求连接所有点的总边长最小的树。数学模型:设点集 P={p1​,…,pn​}⊂R2,边权为欧氏距离 d(pi​,pj​)=(xi​−xj​)2+(yi​−yj​)2​。目标是找到一棵生成树 T=(P,E)使得 ∑(i,j)∈E​d(pi​,pj​)最小。该问题是经典 MST 在欧氏度量下的特例,属于 NP-hard 吗?

weixin_49199313的博客 168

Java与CPU缓存是如何亲密接触的!

在解释【伪共享】这个概念之前,我们先来运行一段代码,小编的电脑上有4个core。 这个程序的逻辑是4个线程共享同一个数组读写不同下标的变量。每个线程循环1亿次读写,也就是+1操作。然后统计4个线程同时跑完总共花的时间。 下面我们来看看在小编的电脑上运行的结果: 然后我把SharingLong里面的注释代码去掉,再跑了一下: 在性能上注释前后差别高达5比1,为什么会在性能...

qq_24047659的博客 167

DataGridView 右键菜单复制(公共)

dataGridView1.CellMouseClick += new DataGridViewCellMouseEventHandler( (sender1, e1) => { if (e1.Button == MouseButtons.Right) {

沁心渃水 2834

cpu 缓存 java_Java与CPU缓存是如何亲密接触的!

在解释【伪共享】这个概念之前,我们先来运行一段代码,小编的电脑上有4个core。这个程序的逻辑是4个线程共享同一个数组读写不同下标的变量。每个线程循环1亿次读写,也就是+1操作。然后统计4个线程同时跑完总共花的时间。下面我们来看看在小编的电脑上运行的结果:然后我把SharingLong里面的注释代码去掉,再跑了一下:在性能上注释前后差别高达5比1,为什么会在性能上会产生如此大的差别呢?这就是本篇要...

weixin_39850697的博客 110

Veritas NetBackup 10.1 Windows安装指南:从账户配置到完整部署

本文详细介绍了Veritas NetBackup 10.1在Windows系统上的完整安装流程,从账户配置到部署验证。重点讲解了创建专用服务账户、安装组件选择、许可证输入等关键步骤,并提供了常见问题排查和进阶配置建议,帮助用户顺利完成企业级备份解决方案的部署。

weixin_26765287的博客 454

【数学基础】基于欧拉角表达的旋转顺序

欧拉角旋转顺序

FalschxMico 1385

java cpu缓存行_Java与CPU缓存是如何亲密接触的!

在解释【伪共享】这个概念之前,我们先来运行一段代码,小编的电脑上有4个core。这个程序的逻辑是4个线程共享同一个数组读写不同下标的变量。每个线程循环1亿次读写,也就是+1操作。然后统计4个线程同时跑完总共花的时间。下面我们来看看在小编的电脑上运行的结果:然后我把SharingLong里面的注释代码去掉,再跑了一下:在性能上注释前后差别高达5比1,为什么会在性能上会产生如此大的差别呢?这就是本篇要...

weixin_36448083的博客 207

详细指南:如何在TI C2000基于C28x的微控制器上部署FreeRTOS_C语言实现

FreeRTOS是一个小型、开源、实时的嵌入式操作系统内核。它为多种微控制器和处理器提供了支持,包括ARM、AVR、MSP430和PIC等。小型和轻量级:适合资源受限的嵌入式系统。实时性:可以满足严格的实时要求。多任务支持:支持多任务并发执行。内存管理:提供了多种内存管理策略。模块化设计:用户可以根据需要选择所需的功能模块。TI C2000系列微控制器是专为实时控制应用设计的。高性能C28x核:提供了高速的浮点运算能力。丰富的外设:包括PWM、ADC、DAC、通信接口等。低功耗。

qq_38334677的博客 1607

vc++64位系统下long的长度为4个字节_Java与系统硬件的亲密接触「伪共享」

作者:码洞来源:https://zhuanlan.zhihu.com/p/32764602在解释【伪共享】这个概念之前,我们先来运行一段代码,小编的电脑上有4个core。这个程序的逻辑是4个线程共享同一个数组读写不同下标的变量。每个线程循环1亿次读写,也就是+1操作。然后统计4个线程同时跑完总共花的时间。下面我们来看看在小编的电脑上运行的结果:然后我把SharingLong里面的注释代码去掉,再跑...

weixin_39969257的博客 486

Briefings in Bioinformatics投稿经验分享

Briefings in Bioinformatics投稿经验分享

LiRongLu_的博客 3340

手把手教你完成基于YOLOv8的水果/商品/食堂/蔬菜/饮料/书籍识别与计价系统,前台+后台管理系统

手把手教你完成基于YOLOv8的商店的水果/商品/食堂/蔬菜/饮料/书籍识别与计价系统,前台+后台管理员系统,可进行目标检测的操作和展示,实现对图片、视频和摄像头的实时目标检测

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 2459

码洞技术文章大全

全栈虚拟机GraalVM初体验基于Netty自己动手编写RPC框架自己动手实现一个极简Web框架水塘抽样与阶层固化深度解析某头条的一道面试题基于Netty实现Redis协...

shellquery的博客 93

Android 15.0 添加自定义服务,并生成jar给第三方app调用

在15.0系统ROM产品定制化开发中,由于需要新增加自定义的功能,所以要增加自定义服务,而app上层通过调用自定义服务,来调用相应的功能,所以系统需要先生成jar,然后生成jar 给上层app调用,接下来就来分析实现的步骤,然后来实现相关的功能 从而来实现所需要的功能

安卓兼职framework和app工程师的博客 670

码洞原创深度技术文章大全 —— 高端面试必备

《快学 Go 语言》第 11 课 —— 协程《快学 Go 语言》第 10 课 —— 错误与异常《快学 Go 语言》第 9 课 —— 接口《快学 Go 语言》第 8 课 —...

codehole_的博客 293

SAP HANA 的一个王炸技能,Smart Data Access

SAP HANA Cloud近期推出Knowledge Graph Engine(KGE),支持RDF三元组存储和SPARQL查询,与向量引擎等组件协同实现混合检索。同时文章介绍了HANA的Smart Data Access(SDA)功能,它通过虚拟表技术实现跨系统联邦查询,支持30+种数据源(不包括PostgreSQL),适用于混合云场景。SDA通过谓词下推优化性能,典型应用包括冷热数据分层和跨系统报表分析。对于PostgreSQL连接需求,建议使用HANA的Smart Data Integration方

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 499

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

上一篇: 有趣的Python开源库之Hashids
下一篇: 会玩Go!会玩Python!嘭!Go-Python!
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值