Java多线程篇(2)——mesi与内存屏障与volatile

news2024/12/29 10:48:27

文章目录

  • CPU高速缓存
    • 高速缓存
    • storeBuffer
    • invalidate message queue
  • JMM 内存屏障
  • volatile

CPU高速缓存

高速缓存

每个cpu核心都有自己的高速缓存,结构如下
在这里插入图片描述
有缓存必有一致性问题,CPU0和CPU1之间的缓存是如何保持一致的。比较常见的一种做法就是 MESI 缓存一致性协议。

MESI 缓存一致性协议

在CPU缓存中最小的存储单元称为缓存行(cache line),一般大小为64B。缓存中每个缓存行都有2个Bit位去存储缓存状态,分别是 M,E,S,I。
在这里插入图片描述
这些标记是如何保证缓存一致性的?
1、写操作时,如果发现写的缓存行是 S,就将该缓存行变成 M,其他缓存行状态变为 I。
2、读操作时,如果发现读的缓存行是 I,就从主存读取新的数据。
3、在任何操作之前(不管是读还是写),如果发现其他缓存行存在 M,就先将其写回主存变成 E。

cpu core是如何感知到其他缓存行的变化的?
每个cpu core都配备了一个总线嗅探器(Bus Snooper),这是一个硬件单元,可以检测总线上的读取和写入操作,以及这些操作所涉及的内存地址。当一个cpu core执行读取或写入操作时,这个操作的数据和内存地址会传输到系统总线上,总线嗅探器会监视总线上的这些数据传输。

storeBuffer

单靠cache对cpu的性能压榨设计者们感觉还不够。考虑这么一种场景:core 1 store数据,此时发送Invalidate message到总线上,其他core收到消息后无效自己的缓存行并回复ack,而core 1需要等到收到其他core的Invalidate message ack,才可以继续往下执行。这意味着core 1存在等待。
身为优秀的设计者,他们无法忍受这种盲等的行为,所以他们引入了 storeBuffer 处于cpu core与cache之间。
在这里插入图片描述
有了store buffer之后core 1如果执行store操作就不用立刻向core2发送invalidate message了,只需要将值添加到store buffer中即可。

注意,此时只是把数据写入store buffer,不会立马更新cache。读数据时如果store buffer存在值,就直接从store buffer获取(store forwarding)。

那么 store buffer 什么时候刷到cache?
当storebuffer满了之后会刷,或者使用 memory barrier(CPU 内存屏障)人为的去控制(比较常见的,在C/C++中,smp_mb()方法将storebuffer中的数据全部刷进cache)。

invalidate message queue

当 storebuffer 刷到 cache 后,同样还是少不了发送 invalidate message 并等待 ack。假如此时其他核心已经很忙了就会导致 invalidate message 处理被延后,导致ack的回复被延后。
设计者为了进一步压榨cpu的性能,引入了 invalidate message queue 。
在这里插入图片描述
这样当发送 invalidate message 后,只需push到对应 invalidate queue 后即可立马回复ack。

那么 invalidate message 什么时候刷到cache?
同样也可以通过 smp_mb() 添加 memory barrier 将 invalidate queue 刷到cache。

smp_mb()是mfence全屏障(将store buffer和invalidate queue都flush一遍),除此之外还有lfence读屏障(将 invalidate queue flush),sfence写屏障(将 storebuffer flush)。


JMM 内存屏障

众所周知,JMM 内存模型分为工作内存,主内存。这里的工作内存和主内存虽然和上面的cpu缓存和主存很像,但严格意义上来说不是一个概念。JMM 的工作内存和主存是逻辑上的划分,并没有特别指定一个具体的实物,以工作内存为例,可以在内存也可以在cpu缓存(绝大部分情况下在CPU缓存)。
同理,JMM 内存屏障也是一种抽象概念,是 JAVA 为了更好的描绘线程之间的内存关系而衍生出来的一种概念,在不同的处理器/操作系统上会有不同的实现。换句话说,即使没有CPU内存屏障,也不影响 JMM 内存屏障的存在与否。

理论上JMM内存屏障长这样:
在这里插入图片描述
实际上,不同的cpu会有不同的JMM内存屏障实现(即不同的OrderAcess实现类)。
x86_linux系统为例,JMM内存屏障长这样:
在这里插入图片描述
其实在x86_linux的实现中只有 storeload 才有CPU内存屏障的效果,其他三个屏障是没有的。

因为加入内存屏障的目的是防止一切重排导致的程序乱序。而x86处理器不会对loadload、loadstore和storestore操作做CPU指令重排序,同时这三种情况也不会发生内存重排序,所以就没必要加CPU内存屏障。

重排序的3种情况:
编译器重排、CPU 指令重排、内存重排(内存重排就是因为引入了上面说的 store buffer 和 invalidate queue 导致 memory order 和 program order 不一致,看起来就像是程序乱序执行了一样)

虽然loadload、loadstore和storestore不会发生CPU层面的重排序,但是编译时期还是有可能发生重排序的。所以对于 loadload,loadstore 调用了 acquire() 方法确保屏障前的读取和屏障后的读写不会编译重排(编译屏障)。对于 storestore 调用了release() 方法确保屏障前的读写和屏障后的写入不会编译重排(编译屏障)。

什么是acquire/release?
acquire:任何读操作不会与其之后的任何读/写操作重排序。
release:任何写操作不会与其之前的任何读/写操作重排序
换句话说,acquire 实现了 LoadLoad + LoadStore,release 实现了 LoadStore + StoreStore。
而x86使用的内存模型是Strong Memory Model(强内存模型),每个机器执行指令默认地包含acquire、release语义,这也正是x86在loadload、loadstore和storestore不会发生CPU层面重排序的原因。
在这里插入图片描述
我的理解是JMM在实现内存屏障时也同样沿用了acquire/release的命名,在不同的cpu实现上可能会有不同的效果,比如x86只有编译屏障的效果,因为x86天然不会重排除storeload外的三种情况。而其他没有“天然支持”的cpu既有编译屏障又有cpu层屏障的效果。

至于 storeLoad,调用了 fence() ,fence实现了全屏障效果(通过 lock 前缀指令实现),所以该屏障开销是最大的。

跟release/acquire类似,fence(通过 lock 前缀指令实现)实现了 LoadStore + StoreStore + LoadLoad + StoreLoad。
lock前缀指令,虽然lock指令不是内存屏障,但是却具有内存屏障的功能:
1、确保lock后面的指令变成一个原子操作。在一些老版本的处理器中,带有lock前缀的指令会锁住总线,使得其他处理器暂时无法通过总线访问共享内存,这会带来昂贵的开销。在后面的处理器,Intel使用缓存锁定(Cache Locking)来保证指令执行的原子性。大大降低lock前缀指令的开销。
2、禁止该指令与之前和之后的读和写指令重排序。
3、会flush store buffer(x86处理器没有 Invalidate Queue,所以不需要flush Invalidate Queue)。

为什么有了MESI还需要JMM内存屏障?
第一:因为CPU的结构不只有cache,还是做了优化的,存在 store buff 和 invalidate message queue,在保证可见性的场景下需要cpu内存屏障flush其内容到cache。第二:编译时期也是可以发生重排序的,所以单靠cpu层面的屏障是不够的,还需要java层面的内存屏障来避免编译时期的重排序。换句话说,JMM内存屏障是应用层的东西,而MESI是硬件层的东西。


volatile

接下来从字节码开始分析volatile。
首先,打印字节码文件,发现volatile变量打上 ACC_VOLATILE 的标记
在这里插入图片描述
然后来到JVM处理标签的地方

volatile写:
bytecodeInterpreter.cpp#run#CASE(_putfield)/CASE(_putstatic)
在这里插入图片描述

volatile读:
bytecodeInterpreter.cpp#run#CASE(_getfield)/CASE(_getstatic)
在这里插入图片描述
把上面的截图用伪代码总结一下就是

if(属性读指令){
     if(属性被volatile修饰){
         OrderAccess::load_acquire
     }else{
         正常读
     }
}
else if(属性写指令){
    if(属性被volatile修饰){
        OrderAccess::release_store
        OrderAccess::storeload
    }else{
        正常写
    }
}

什么是load_acquire,什么是release_store?
在JVM源码中的 orderAccess.hpp 可以找到相关的注释
在这里插入图片描述
我的理解是 acquire = LoadLoad屏障 + LoadStore屏障
而 load_acquire = load + acquire ,即等价于 load指令 + LoadLoad屏障 + LoadStore屏障。
同理 release = LoadStore + StoreStore屏障
而 release_store = release + store ,即等价于 LoadStore屏障 + StoreStore屏障 + store指令。

所以总的来说从代码的解释上看就是
volatile读之后加 LoadLoad,LoadStore
volatile写之前加 LoadStore,StoreStore,写之后加 StoreLoad

理论上volatile写之后,只有当后续有volatile读才需要插入storeLoad屏障。但是,多线程环境下编译器无法确定volatile写后,其他cpu是否有volatile读操作,所以voliate写后一律加上storeLoad。

这与 Doug Lea 总结的 JSR 133 相关的规范是一致的(原本链接:https://gee.cs.oswego.edu/dl/jmm/cookbook.html)
在这里插入图片描述
不过这篇文章的最后,有提到实际上JMM是如何插入内存屏障的,如下
在这里插入图片描述
可以发现,volatile 写之前只插入了storestore屏障,并没有loadstore屏障。

同理看下面这张图,引用自《java并发编程的艺术》- 3.4.4 volatile内存语义的实现
在这里插入图片描述
《java并发编程的艺术》跟上面 Doug Lea 表达的观点也是一样的。
在规范上都说volatile写前面的任何操作都禁止与volatile重排,即意味着普通读和volatile写也不能重排,也就是说规范上应该要加loadstore和storestore两个屏障才对。但一提到实际上是如何插入屏障的,又说volatile写前面只插入storestore屏障。
这到底是为什么?volatile写之前到底需不需要loadstore屏障?
说实话我也不知道。我只能就此表达一些我自己的短见。
我觉得这是规范与实际实现上的差异,就是规范上的说法是一个适用于任何场景任何cpu的完美解决方案。但是在实际实现上,比如在x86,开发人员发现即使普通读和volatile写重排了也不会违反happens-before原则,所以就没加loadstore屏障以提高性能。

个人水平有限,文中所有内容都仅代表我个人观点,如有不对,欢迎指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1009768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql convert函数 解决读取double为科学计数法问题

convert顾名思义就是转化, cast差不多 MySQL CONVERT() 函数 | 参考手册 为什么需要这个函数? mysql是弱类型的 where stringcol1 and intcol1 都行 会自动转化,那我为什么还要呢? mysql有个类型是double ,基本没人…

第二本书交稿了

大家好,我是飞哥! 就在刚刚,我把新书的markdown源文件提交给出版社老师了,也就是说新书正式交稿了! 上一本咱们书咱们写的是Linux网络方面的,书名是《深入理解Linux网络》。 咱们这本书帮助很多之前惧怕内核…

美东一公司的郁闷面试题

说是题目可以用不同的语言,但是貌似 Java 是多线程的,用 Java 写肯定容易不少。 但,觉得这个题目用多线程简直是有点脱了裤子放屁。 完整题目内容 题目的网站内容如下: Please complete the following challenge in one of th…

使用这款免费的洗稿软件究竟是好还是坏

嘿,大家好!今天,我们要聊一聊一个备受争议的话题:洗稿软件(147SEO改写软件)。这些软件声称可以帮助你在写作时省下不少功夫,但究竟是好还是坏呢? 咱们都知道,写作是一门艺术,也是一项…

华策影视AIGC工程师招聘; 百度大模型创业松;主流大语言模型的技术原理细节;AIGC Prompt的七个缺陷 | ShowMeAI日报

👀日报&周刊合集 | 🎡生产力工具与行业应用大全 | 🧡 点赞关注评论拜托啦! 🎯 华策影视AIGC工程师招聘,AIGC在「文娱领域」的真正落地 逛即刻时发现关注的AI博主 杨昌 发布了自己公司的招聘信息&#x…

Postgresql中检测内存越界或use after free的简便方法

1 使用场景 在Postgresql的内存管理模块中,最常用的aset.c提供的内存池实现,该实现提供了两个非常实用的开关来解决常见的内存越界问题: memdebug.c * About CLOBBER_FREED_MEMORY:** If this symbol is defined, all freed memory is over…

git之工作中实际应用篇(非常适合刚到公司上班但是git不太熟又不好意思问同事的友友)

目录 前言当你刚到一个公司写代码,交自己的分支提交并推送了代码,但是有所更改第二天拉取代码切换分支远程分支太多&&本地仓库太多checkout检出失败的情况未完待续 前言 此篇用于记录笔者在工作中用到git遇到的问题及大部分的操作。 区别于理论…

无涯教程-JavaScript - FALSE函数

描述 FALSE函数返回逻辑值FALSE。 语法 FALSE () 争论 FALSE函数没有参数。 Notes 您还可以在工作表或公式中直接键入FALSE单词,Microsoft Excel会将其解释为逻辑值FALSE。 提供FALSE功能主要是为了与其他电子表格程序兼容。 适用性 Excel 2007,Excel 2010,Excel 2013…

【用unity实现100个游戏之11】复刻经典mirror消消乐游戏

文章目录 前言开始项目开始一、方块网格生成二、方块交换三、添加交换的动画效果四、水平消除检测五、垂直消除检测六、完善删除功能七、效果优化(移动方块后再进行消除检测)八、方块下落十、方块填充十一、后续 源码参考完结 前言 欢迎来到经典消消乐游…

数据清洗:数据挖掘的前期准备工作

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ 🐴作者:秋无之地 🐴简介:CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作,主要擅长领域有:爬虫、后端、大数据…

释放潜能!RunnerGo:性能测试的全新视角

在数字化时代,性能测试已成为企业持续发展的关键一环。但面对繁杂的工具和流程,很多企业却陷入了无从选择的困境。现在,一款名为RunnerGo的全新性能测试工具正悄然崭露头角。 RunnerGo,一款由国内开发者自主研发的全栈式性能测试…

最新模块化设计小程序系统源码完整版:开源可二开,支持DIY

随着互联网的快速发展,小程序已成为各行各业开展业务的重要工具。而模块化设计小程序系统源码完整版则是一种高效、灵活、易维护的解决方案。 分享一个最新的模块化设计小程序系统源码完整版,源码开源可二开,支持自由DIY设计,含完…

华为云新用户:定义,优惠券及专享活动

在当今的数字化时代,云计算已成为企业与个人的必备服务。华为云,作为全球领先的云计算服务提供商,吸引了众多新用户的关注。本文将详细介绍华为云新用户的定义、优惠券及专享活动相关内容,帮助大家更好地了解华为云新用户优惠政策…

win10win11截图技巧——不用安装其他截图工具或者运行其他截图工具,就可以截图,win10和win11可用

快捷键shift wins可以调出来windows自带的截图工具。 测试了一下win10和win11都可以用。 可以截图的方式有: 1,全屏截图, 2,窗口截图, 3,任意截图, 4,画矩形截图 以下内容来自…

算法竞赛入门【码蹄集新手村600题】(MT1280-1300)C语言

算法竞赛入门【码蹄集新手村600题】(MT1280-1300)C语言 目录MT1281 N的M次方MT1282 Disarium数MT1283 区间Disarium数MT1284 快乐数MT1285 忠实数MT1286 忠实数序列MT1287 ASCII值MT1288 谁在说谎MT1289 调和级数不等式MT1290 级数MT1291 级数IIMT1292 某级数MT1293…

一百七十七、Hive——海豚调度执行Hive的.sql文件

一、目的 对于Hive数仓,每一层的建库建表SQL语句都各自放在一个.sql文件里,然后用海豚调度执行一下Hive的.sql文件 二、实施步骤 (一)第一步,上传.sql文件到海豚调度器上 (二)第二步&#xf…

分享一个java技术开发的springboot线上问卷调查可视化系统源码 lw 调试

💕💕作者:计算机源码社 💕💕个人简介:本人七年开发经验,擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等,大家有这一块的问题可以一起交流! 💕&…

帝国CMS的Sitemap网站地图生成插件下载及教程

具体使用方法: 第一步:解压下载 stitemap 压缩包; 第二步:修改config.php里的网址为自己的网址。 1、如果网站站点是响应式的直接选择pc即可。 2、如果电脑站和手机站不一样则都需要修改。 第三步:将解压出的“sitema…

4.docker容器编排(docker compose 与 docker swarm)

本文目录 1.容器编排2.Docker Compose1.Docker Compose 安装2.Docker Compose 示例1.使用 docker-compose 启动 nginx2.docker compose 常用命令3.校验 docker-compose.yml 是否有错误4.创建服务&#xff0c;启动容器5.弹性伸缩<扩缩容> 3.Docker Swarm1.Swarm 架构图2.S…

华为云云耀云服务器实例管理域名

目录 域名概述 通过域名访问网站流程 域名注册、解析和备案的关系 域名解析 如何修改 域名概述 部署一个网站或Web应用后&#xff0c;若要使该网站能够在Internet上通过域名直接访问&#xff0c;需要为网站注册域名、备案以及配置解析。 通过域名访问网站流程 1、注册域…