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的博客 405

JAVA中的伪共享与缓存行

JAVA中的伪共享与缓存行

qq_45443475的博客 1870

Java对象内存布局

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

诺浅的专栏 1069

[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

YOLOv8模型RKNN量化策略与RK3588部署实战解析

本文详细解析了YOLOv8模型在RK3588平台上的RKNN量化策略与部署实践。通过对比INT8和FP16量化方式的性能差异,结合实际测试数据,为开发者提供了从模型导出、转换到板端部署的完整解决方案,帮助实现高效的边缘AI应用部署。

oil88的博客 1109

人工势场法在自动驾驶避障中的实践与优化

本文深入探讨了人工势场法在自动驾驶避障中的核心原理与实践优化。通过解析引力与斥力模型,揭示了经典算法在局部最优和目标不可达等问题上的局限性,并重点介绍了改进斥力场函数、引入道路边界斥力及动态参数调整等关键优化策略,有效提升了该局部路径规划算法在复杂动态场景下的实用性与鲁棒性。

weixin_29208327的博客 927

C++ long数据类型:超长整型数据的存储和计算

long数据类型是一个整型,存储4个字节,可以表示一个范围为-2,147,483,648到2,147,483,647的整数。在64位系统上,long长度为8个字节。如果需要存储更大范围的整数,可以使用long long数据类型,它能够存储更大的整数。long long数据类型存储8个字节,可以表示-9,223,372,036,854,775,808到9,223,372,036,854,775,807的整数。

很酷的站长的博客 8359

自偏置电流镜设计实例

自偏置电流镜设计实例 一、概述 ​ 众所周知,电流镜属于模拟电路设计中的一个基本模块,可为其他模块提供偏置电流,例如在放大器中提供偏置电流以形成尾电流源并确定直流偏置点。本文将会介绍一种自偏置的电流镜,从原理出发,并以0.18um的工艺进行设计和仿真。 二、原理 ​ 如图一所示的电路,也称之为Beta-multiply reference电路,其中M2的尺寸是M1的k倍。M3复制M4的电流,M2和M1的漏极电流相同,但是VGS并不相等。其中 VGS=2IμnCoxW/...

aonuanjiang的博客 1万+

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

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

qq_24047659的博客 167

STM32HAL库 基于外部Flash+FatFs模拟U盘方式进行固件拷贝升级(详解)

STM32HAL库 基于外部Flash+FatFs模拟U盘方式进行固件拷贝升级

qq_36075612的博客 1万+

部署OpenClaw时设置环境变量提示export: not valid in this context的完整解决方案

OpenClaw环境变量设置错误解决方案 问题概述 在部署OpenClaw时设置环境变量出现"export: not valid in this context"错误,通常由以下原因导致: 在非交互式shell中使用export命令 在命令替换或管道中使用export 脚本文件包含Windows换行符(CRLF) 解决方案 修正脚本执行方式:确保export命令在正确的shell上下文中使用 使用source命令:通过. setup_openclaw.sh或source setup_op

博客 459

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

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

weixin_39850697的博客 110

把收款差异从扯皮变成闭环:SAP S/4HANA Dispute Resolution 模块到底解决什么问题?

本文摘要: SAP S/4HANA的争议解决模块(Dispute Resolution)通过标准化流程管理企业应收争议,解决发票与收款差异问题。核心是创建可追踪的Dispute Case工单,明确处理角色(Processor和Manager),并与财务系统联动实现自动清账。系统支持手工/自动创建争议工单,通过动态默认值减少错误,并最终实现争议金额归零的闭环管理。文中给出制造业客户扣款的完整处理案例,并强调组织落地需统一权限与目录。最后提供ABAP代码示例展示如何通过BAPI创建争议工单。(150字)

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

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

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

weixin_36448083的博客 207

正点原子stm32f103RChal库制作数字秒表:如用keil5和正点原子STM32F1开发板实现?

🏆本文收录于 《全栈Bug调优(实战版)》 专栏,该专栏专注于分享我在真实项目开发中遇到的各类疑难Bug及其深层成因,并系统提供高效、可复现的解决思路和实操方案。无论你是刚入行的新手开发者,还是拥有多年项目经验的资深工程师,本专栏都将为你提供一条系统化、高质量的问题排查与优化路径,助力你加速成长,攻克技术壁垒,迈向技术价值最大化与职业发展的更高峰🚀!    📌 特别说明: 文中部分技术问题来源于真实生产环境及网络公开案例,均经过精挑细选与系统化整理,并结合多位一线资深架构师和工程师多年实战经验沉淀,提

**My Coding Family** 1266

SXU—软件测试方法和技术-朱少民-第四部分 软件测试流程和规范-附考点

软件测试方法和技术-朱少民-第四章 软件测试流程和规范—这本书最后一部分的考点整理,考点分析

weixin_63597914的博客 1752

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

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

weixin_39969257的博客 486

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

码洞技术文章大全

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

shellquery的博客 93

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

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

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

codehole_的博客 293

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

工作中使用到的单词(软件开发)

1.エイリアス   alias 别名 powershell使用 get-help 命令时,会得到与此命令 相关(别名)的命令 2.プロキシ proxy 代 理 3.プロモート   promote 促进,提升   开发的系统新功能上线发布,有时会使用到这个词,比如 STプロモート 还有使用リリース这个词的。比如STリリース。个人认为两者含义相同。 4.見逃し み................................................................

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

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值