内存一致性,指令重排序,内存屏障,volatile解析

news2025/1/20 12:08:39

文章目录

    • 为什么会存在“内存可见性”问题
    • 重排序与内存可见性的关系
    • as-if-serial语义
      • 单线程程序的重排序规则
      • 多线程程序的重排序规则
    • happen-before是什么
    • 解决方案:内存屏障
    • Volatile关键字解决内存可见性问题的实现原理

为什么会存在“内存可见性”问题

下图为x86架构下CPU缓存的布局,即在⼀个CPU 4核下, L1、 L2、 L3三级缓存与主内存的布局。每个核上⾯有L1、 L2缓存, L3缓存为所有核共⽤。

在这里插入图片描述

由上图我们可以知道,CPU中每个核在执行线程时,都会创建一个本地缓存来保存主内存中的数据。在修改后,再异步写会给主内存。多个线程间对于变量的交互,是通过主内存来进行的。那么这个过程中,就存在一个多个线程间本地缓存同步的问题。比如说,现在对于一个主内存中的变量x=3,线程A将该值修改为x=4,但是,在线程A将数据同步到主内存前,线程B中就读取了数据。那么线程B中就讲讲本该x=4的值读成了x=3,造成了线程间同步的错误。

上述的例子中,涉及到了多线程间缓存的一致性问题,同时,线程B的读线程先于线程A的写线程执行,这种错误也叫指令重排序的问题。接下来,这对这几张问题,我们进行详尽的讨论和分析。

重排序与内存可见性的关系

上述例子中,指令的执行顺序和写入主内存的顺序不完全一致的问题,是重排序的一种,也成为内存重排序。除此之外,还有编译器和CPU的指令重排序。

重排序类型:

  1. 编译器重排序。 对于没有先后依赖关系的语句,编译器可以重新调整语句的执⾏顺序。
  2. CPU指令重排序。 在指令级别,让没有依赖关系的多条指令并⾏。
  3. CPU内存重排序。 CPU有自己的缓存,指令的执行顺序和写⼊主内存的顺序不完全⼀致。

在三种重排序中,第三类就是造成上述“内存可见性”问题的主因,如下案例:

线程1:

X=1
a=Y

线程2:

Y=1
b=X

假设X、 Y是两个全局变量,初始的时候, X=0, Y=0。请问,这两个线程执完毕之后, a、 b的正确结果应该是什么?

很显然,线程1和线程2的执行先后顺序是不确定的,可能顺序执行,也可能交叉执行,最终正确的结果可能是:

1. a=0,b=1
2. a=1,b=0
3. a=1,b=1

也就是不管谁先谁后,执行结果应该是这三种场景中的⼀种。但实际可能是a=0, b=0。

两个线程的指令都没有重排序,执行顺序就是代码的顺序,但仍然可能出现a=0, b=0。原因是线程1先执行X=1,后执行=Y,但此时X=1还在自己的本地缓存里面,没有及时写⼊主内存中。所以,线程2看到的X还是0。线程2的道理与此相同。

虽然线程1觉得自己是按代码顺序正常执行的,但在线程2看来, a=Y和X=1顺序却是颠倒的。指令没有重排序,是写入内存的操作被延迟了,也就是内存被重排序了,这就造成内存可见性问题。

as-if-serial语义

在解决上述重排序的问题前,先来了解一下CPU的指令重排序

我们知道,当代码被编译执行之后,CPU为了提高执行效率,会对代码进行一定程度的重排序。比如,对于代码中,两个互相不影响的部分,如果CPU执行重排序,由顺序执行重排序为并行执行,就会在一定程度上提高执行效率。

那么一个问题就是:重排序的原则是什么?什么场景下可以重排序,什么场景下不能重排序呢?

单线程程序的重排序规则

⽆论什么语⾔,站在编译器和CPU的⻆度来说,不管怎么重排序,单线程程序的执⾏结果不能改变,这就是单线程程序的重排序规则。

即只要操作之间没有数据依赖性,编译器和CPU都可以任意重排序,因为执行结果不会改变,代码看起来就像是完全串行地⼀行行从头执行到尾,这也就是as-if-serial语义。

对于单线程程序来说,编译器和CPU可能做了重排序,但开发者感知不到,也不存在内存可⻅性问题。

多线程程序的重排序规则

编译器和CPU的这⼀行为对于单线程程序没有影响,但对多线程程序却有影响。

对于多线程程序来说,线程之间的数据依赖性太复杂,编译器和CPU没有办法完全理解这种依赖性并据此做出最合理的优化。

编译器和CPU只能保证每个线程的as-if-serial语义。

线程之间的数据依赖和相互影响,需要编译器和CPU的上层来确定。

因此,对于多线程下的重排序来说,需要设计方案来告知编译器和CPU在多线程场景下什么时候可以重排序,什么时候不能重排序。

happen-before是什么

使用happen-before描述两个操作之间的内存可见性。

如果A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。

A happen before B不代表A⼀定在B之前执行。因为,对于多线程程序而言,两个操作的执行顺序是不确定的。

happen-before 只确保如果A在B之前执行,则A的执行结果必须对B可见。定义了内存可见性的约束,也就定义了⼀系列重排序的约束。

针对上述的规范,java定义了一套自己的内存模型规范:内存模型(JMM),在多线程中

  • ⼀方面,要让编译器和CPU可以灵活地重排序。
  • 另⼀方面,要对开发者做⼀些承诺,明确告知开发者不需要感知什么样的重排序,需要感知什么样的重排序。

根据需要决定这种重排序对程序是否有影响。如果有影响,就需要开发者显示地通过volatile、synchronized等线程同步机制来禁止重排序。

基于happen-before的这种描述⽅法, JMM对开发者做出了⼀系列承诺:

  1. 单线程中的每个操作, happen-before 对应该线程中任意后续操作(也就是 as-if-serial语义保证)。
  2. 对volatile变量的写⼊, happen-before对应后续对这个变量的读取。
  3. 对synchronized的解锁, happen-before对应后续对这个锁的加锁。
    ……

JMM对编译器和CPU 来说, volatile 变量不能重排序;非volatile 变量可以任意重排序。

解决方案:内存屏障

为了禁止编译器重排序和 CPU 重排序,在编译器和 CPU 层面都有对应的指令,也就是内存屏障(Memory Barrier)。这也正是JMM和happen-before规则的底层实现原理

内存屏障就是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。

编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。

而CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。

内存屏障是很底层的概念,对于 Java 开发者来说,⼀般用volatile 关键字就足够了。 后面会详细讲解volatile关键使用内存屏障解决内存可见性问题的原理。

在理论层⾯,可以把基本的CPU内存屏障分成四种:

  1. **LoadLoad:**禁止读和读的重排序。该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
  2. **StoreStore:**禁止写和写的重排序。该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
  3. **LoadStore:**禁止读和写的重排序。确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
  4. **StoreLoad:**禁止写和读的重排序。该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

StoreLoad同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。

有了内存屏障,针对上述案例中,线程A和线程B的可见性问题和内存重排序问题,就可以在线程A的写和线程B的读之间加一层StoreLoad内存屏障,来禁止写和读的重排序。也就是线程B的读一定在线程A的写之后执行,从而让两个线程之间代码的执行有了正确的顺序性。

Volatile关键字解决内存可见性问题的实现原理

volatile关键字解决内存可见性问题是利用内存屏障来实现的。

下面我们来看看 volatile 读 / 写时是如何插入内存屏障的。volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰后,那么就具备了两层语义

  1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是 立即可见 的。
  2. 禁止进行指令重排序。

Volatile关键字的作用:

  • 使用 volatile 关键字会强制将修改的值立即写入主存。
  • 缓存行无效(反映到硬件层的话,就是CPU的 L1 或者 L2 缓存中对应的缓存行无效)。

Volatile关键字的实现原理

由于不同的CPU架构的缓存体系不⼀样,重排序的策略不⼀样,所提供的内存屏障指令也就有差异。
这⾥只探讨为了实现volatile关键字的语义的⼀种参考做法:

  1. 在volatile写操作的前⾯插⼊⼀个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  2. 在volatile写操作的后⾯插⼊⼀个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  3. 在volatile读操作的后⾯插⼊⼀个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。

具体到x86平台上,其实不会有LoadLoad、 LoadStore和StoreStore重排序,只有StoreLoad⼀种重排序(内存屏障),也就是只需要在volatile写操作后⾯加上StoreLoad屏障。

对于添加了volatile关键字的变量,在指令执行顺序中的过程如下图所示:
在这里插入图片描述

在这里插入图片描述

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

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

相关文章

redis 的企业实战应用 (二)

前言: 如今redis的常用场景有 短信登录:使用redis共享session来实现 商户查询缓存:会理解缓存击穿,缓存穿透,缓存雪崩等问题,让小伙伴的对于这些概念的理解不仅仅是停留在概念上,更是能在代码…

【数学】仿射变换

∣降维打击NightguardSeries.∣\begin{vmatrix}\Huge{\textsf{ 降 维 打 击 }}\\\texttt{ Nightguard Series. }\end{vmatrix}∣∣∣∣∣​ 降 维 打 击 Nightguard Series. ​∣∣∣∣∣​ 注:本文讨论的仿射变换仅为y轴上的伸缩变换,且难度在高中生理…

H3CNE V7.0 视频教程

构建中小企业网络全套PPT汇总【V7版本】 第1章 计算机网络概述 第2章 OSI参考模型与TCP IP模型 第3章 局域网基本原理 第4章 广域网基本原理 第5章 IP基本原理 第6章 TCP和UDP基本原理 第7章 路由器、交换机及其操作系统介绍 第8章 命令行操作基础 第9章 网络设备文件…

mycat-3-实战篇

1 总结: 1:用的表必须在mycat的配置文件中配置。 2:mycat默认分片策略中,都是针对表的主键,默认是id,如果主键不是id的,请去rule.xml自己复制一份修改 3: 2 注意细讲解 1:schem…

Springboot启动流程分析(四):完成启动流程

目录 一 添加BeanPostProcessors到IOC容器 二 国际化支持 三 初始化监听器的多路播放器 四 刷新容器 五 注册监听器到IOC容器的多播器 六 完成bean的大规模实例化 6.1 大规模实例化bean 6.1.1 连续三层do...while循环作用 6.1.2 FactoryBean是什么?为什么要…

04 YAML kubetnetes世界里的通用语

文章目录1. 前言2. 声明式和命令式是怎么回事?3. 什么是YAML?4. 什么是API对象?4.1 k8s都有哪些资源对象4.2 列出kubectl 命令详细执行过程5. 如何描述 API 对象5.1 命令式5.2 声明式5.2.1 声明式YAML语法详解5.2.1.1 header部分详解5.2.1.2 …

【教学类-19-01】20221127《ABAB式-规律排序-A4竖版2份》(中班)

展示效果: 单人使用样式: 单页打印样式 ​ 背景需求: 中班幼儿需要掌握ABAB规律排序,如下图所示,AB两个元素能外形不同、颜色不同。 ​ ​利用Python Word单元格填色功能,随机生成AB样式,引…

STM32模拟IIC与IIC四种实现数字光强采集模块GY30(标准库与HAL库)

目录 代码实现是的IIC通信,数据采集后在串口显示,方便大家实现二次开发 原件选择 GY-30 数字光强度介绍 BH1750芯片参数 引脚说明 BH1750指令集 接线表设计 通过四种方式实现GY-30数据采集 1.标准库模拟IIC实现GY-30采集并串口1显示 2.标准库IIC…

重构uniapp uni-ui coloerUI项目

重构uniapp uni-ui coloerUI项目这里写自定义目录标题重构uniappuni-uicoloerUI项目起源流程重构uniappuni-uicoloerUI项目 起源 从网上复制了若依移动端的代码,但是对里面的文件夹布局方式和第三方组件库引入方式不甚了解,就想着从头创建一个空白项目,然后一步一…

Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效

记录:343 场景:在CentOS 7.9操作系统上,开机启动就执行自定义的命令,配置rc.local文件达到需求;在普通用户中配置环境变量开机启动生效,使用profile实现。 版本: 操作系统:CentOS…

01、Docker入门

目录 1、Docker是什么 2、Docker与虚拟化 3、Docker虚拟化的好处 好处一:应用部署方便 好处二:服务器同等配置,性能更优,利用率更高 4、核心概念 5、CentOS7 安装docker(在线方式) 6、镜像 7、Docker容器 8、查看Docker容…

typescript 八叉树的简单实现

查了一些文章,准备自己用typescript写一个简单的八叉树场景管理。 所谓的简单,就是所有元素都是边长为1的立方体。 元素类和树节点类 //元素类,因为都是边长为1的立方体,所以就用cube命名 export class CubeData {public reado…

由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开

一、问题描述 在使用Windows的远程桌面工具连接WindowsServer2016服务器时,无法连接到服务器,并且提示【由于没有远程桌面授权服务器可以提供许可证,远程回来连接已经断开。请跟服务器管理员联系】。 二、解决办法 2.0、前提 Windows Serv…

黑胶歌曲没权限,看我python大展神通,一分钟一个歌单

前言 大家早好、午好、晚好吖 ❤ ~ 人之初,喜白嫖。 大家都喜欢白嫖,我也喜欢,那么今天就来试试怎么白嫖抑云~ 一、需要的准备 1、环境 Python3.6以上 pycharm2019以上 2、模块 requests # 发送请求模块 第三方模块 exec js # 调用JS的…

CocosCreater 教程(下)

1.物理系统 1.1 2D刚体 刚体是组成物理世界的基本对象。 1.2 2D 碰撞组件 目前引擎支持三种不同的碰撞组件: 盒碰撞组件(BoxCollider2D)、圆形碰撞组件(CircleCollider2D) 和 多边形碰撞组件(PolygonCo…

Java中的抽象类和接口

java中的抽象类和接口抽象类什么是抽象类?抽象的使用场景抽象类的案例抽象类的特征、注意事项小结抽象类的应用知识:模版方法模式接口接口概述、特点接口的基本使用:被实现接口与接口的关系:多继承JDK8开始接口新增方法接口的注意…

AtCoder Beginner Contest 277 F. Sorting a Matrix(拓扑排序+虚点)

题目 n*m(2<n,m<1e6,n*m<1e6)的矩阵&#xff0c; 第i行第j列元素a[i][j](0<a[i][j]<n*m) 对于值为0的元素&#xff0c;你可以将其赋值为任意正整数&#xff0c; 不同位置的0元素&#xff0c;可以被赋值成不同的正整数 然后&#xff0c;你可以执行以下操作若…

firefly3399 移植linux5.15.80 - 2022-11-27

需要注意的是&#xff0c;虚拟机需要足够的硬盘空间&#xff0c;不小于15GB&#xff01;&#xff01; 一、内核源码下载 国内镜像地址 git clone https://kernel.source.codeaurora.cn/pub/scm/linux/kernel/git/stable/linux.git/ 基本达到了带宽的最大值。 国外地址&#…

Android使用AudioTrack播放WAV音频文件

目录 1、wav文件格式 2、wav文件解析 3、wav文件播放 QA&#xff1a; 开始播放wav的时候使用了系统的播放器mediaplayer进行播放&#xff0c;但是无奈mediaplayer支持的实在不好。 好些年前自己做过pcm播放使用的是audiotrack&#xff0c;参考&#xff1a;CSDN 其实两者之…

php 进程池设计与实现,phper必学!

php 进程池设计与实现phper 为什么要学习进程池池的概念为什么要有进程池?动态创建进程缺点进程池的优点选择子进程为新任务服务的方式进程池模型服务端客户端结语phper 为什么要学习进程池 在php开发过程中经常使用的 php-fpm 使用的进程模型就是进程池&#xff0c;学习进程…