并发编程——5.JMM、可见性和有序性及volatile的底层实现原理

这篇文章我们来讲一下JMM和其相关的内容。

目录

1.JMM模型的介绍

2.volatile的底层原理

3.有序性的介绍

3.1as-if-serial原则

3.2happen-before原则

4.内存屏障

5.小结


1.JMM模型的介绍

首先,我们来看一下JMM模型。

这是一张多核CPU的并发缓存架构图。我们的数据存在主内存RAM中,由于CPU的运算速度非常快,而CPU从主内存中读取数据的速度比较慢(与前者的速度是差几个量级的),所以为了适配这二者的速度差异,我们在CPU中开辟了一块缓存区,空间不大,里面放的是CPU中使用频率较高的数据,CPU从缓存区中读取数据的速度就比从主内存中读取数据的速度要快的多,这样就便于我们CPU的运行。我们的JMM模型就与上面的多核CPU并发缓存架构类似。

Java多线程内存模型(简称JMM)cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

如下图所示:

下面举例来解释一下:

假设主内存中有一个boolean类型的变量flag,初始值为true,现在我们的线程1要将这个flag改为false,它会先把这个flag复制一份到线程1的工作内存,然后在工作内存中将这个flag改为false。此时线程2和线程3中不一定会感知到这个flag为false。也就是说,我们的线程1将flag改为了false,但是我们的线程2和线程3中的flag还是true。这就是不满足线程的可见性。

下面我们来看一下程序:

解释一下:

首先是定义了一个共享变量initFlag,初始值为false,然后是main方法,里面new了一个线程,线程里面打印一句话,然后是一个死循环,然后线程启动。然后是主线程睡眠2s,然后又new了一个线程,线程里面调用一个方法,方法里面打印一句话,然后修改initFlag的值,然后再打印一句话。正常情况下,initFlag值被修改后,线程1中的死循环会跳出来,会打印success那句话。

但是结果结果显然不是这样的,根据结果我们可以知道,线程2中的所有内容都执行完了,但是线程1中的死循环还没有结束,那句success还没打印出来。那就说明线程2修改后的initFlag值没有被线程1感知到,所以线程1中的死循环没有结束。这就符合我们上面的JMM的讲解了。

那怎么解决呢?给我们的共享变量加一个volatile即可!

如下图所示:

这样问题就解决了

2.volatile的底层原理

上面我们讲了volatile可以解决可见性的问题,下面我们来看一下volatile的底层原理。

在讲volatile的底层原理之前,我们先来了解一下JMM的数据原子操作

如下图所示:

下面通过一个具体的例子来讲解一下

如下图所示(例子是上面initFlag的例子):

首先,主内存中存了 变量initFlag,初始值为false,然后线程1通过总线读取到initFlag,即read操作,然后是load操作,将initFlag写入工作内存中,然后是use操作,对应程序中就是进行判断。同一时刻,线程2也在进行这些操作,不过对应到程序中,线程2的use操作就是改值,然后线程2进行assign赋值操作,将新的initFlag值赋值到线程2的工作内存中的变量中,此时线程2中的initFlag才变为true,然后是store存储操作,即线程2将工作内存中的initFlag值存入主内存中,注意,此时主内存中原本的initFlag值还依然为false,等到最后一步write写入操作,才将主内存中的initFlag值改为true。

但是在线程2进行后面的一系列操作时,线程1中的initFlag值始终为false,并且线程1始终在使用这个initFlag的值,这就是不可见性。

那volatile到底是怎么保证我线程2在修改完initFlag值的同时,我线程1也能感知到并及时修改的呢?

首先,我们来了解两点内容:

然后,我们来看一张图,然后来解释一下:

首先说明一点,这个缓存一致协议是硬件上面的内容。

它的流程是这样的:当我们的线程2修改了initFlag的值之后,也就是执行了assign赋值操作后,它会瞬间触发后面的store存储和write写入这两个操作,也就是说,当某个CPU修改了工作内存里面的数据后,它会马上就将数据同步到主内存中。而其他的CPU通过总线嗅探机制会感知到自己缓存中数据的变化,然后将自己缓存中的数据判为无效数据,然后再重新从主内存中拿数据。

下面了解一下缓存一致协议(了解即可):

那volatile到底是怎么实现上面的那一套功能的呢?

我们来看下面的这张图:

简单来说就是:volatile的底层实现上会有一个汇编的lock前置指令,而这个汇编的lock前置指令会实现硬件层级的缓存一致协议,而缓存一致协议就是那些巴拉巴拉......的东西了

3.有序性的介绍

下面介绍并发三大特性中的有序性。

如下图所示:

简单来说就是:一般情况下,我们的程序是按照我们所写的每一行代码的顺序来运行的,但是有时候,为了提供程序的运行效率,计算机会将我们所写的代码编译为汇编语言后,改变我们所写代码是顺序,然后再运行,这也叫指令重排序,这就会导致在并发的情况下出现错误。

3.1as-if-serial原则

as-if-serial语义:

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-seriali语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

3.2happen-before原则

只靠sychronized和volatle关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据。

happens-before原则内容如下

  1. 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  3.  volatile规则:volatile变量的写,先发生于读,这保证了volatle变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  4. 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  5. 传递性:A先于B,B先于C那么A必然先于C
  6. 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  7. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  8. 对象终结规则:对象的构造函数执行,结束先于finalize()方法

4.内存屏障

下面讲一下Java语言规范的内存屏障。

什么是内存屏障?

简单来说就是,如果这两行代码之间可能发生指令重排序,但是你不想让他们发生指令重排序,那么你就需要在这两行代码之间加上一行代码来防止它们进行指令重排序。加的这行代码就是内存屏障。

内存屏障是什么样的?

如下图所示:

其中的Load、store是Java内存模型的数据原子性操作。

怎么用这个内存屏障?

这个不用你操心,volatile已经帮你用好了。volatile的底层实现上是会有一个汇编的lock前缀,而这个lock前缀就已经实现了内存屏障。

5.小结

这篇文章我们主要讲了JMM,即Java内存模型,讲了JMM数据的原子性操作,讲了volatile的底层实现原理,讲了缓存一致协议,讲了有序性,讲了有序性的两大规则,讲了内存屏障。

内容很散,需要理解,需要自己把这些散的内容串起来。
 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CloudCompare——win11配置CloudComPy

CloudComPy配置 1 基本环境介绍2 安装Anaconda2.1 下载anaconda2.2 安装anaconda2.3 配置镜像源2.4 更改虚拟环境的默认创建位置2.5 其他问题2.5.1 激活自己创建的环境提示:系统找不到指定的路径2.5.2 InvalidVersionSpecError: Invalid version spec: 2.72.5.3 卸载…

JS-27-操作表单

用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。 不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。 一、HTML表单的输入控件 H…

Capture One 23 Enterprise for Mac中文版 全面的图像处理工具

Capture One 23 Enterprise for Mac中文版一款专业的图像编辑和管理软件,具备强大的功能和工具,适用于摄影师、摄影工作室和专业用户。 软件下载:Capture One 23 Enterprise for Mac中文版下载 该软件为用户提供了全面的图像处理工具&#xf…

fastapi的安装

使用pip安装 安装fastapi的语句 pip install fastapi 可以使用国内阿里云镜像源进行安装,会快很多 pip install fastapi -i https://mirrors.aliyun.com/pypi/simple api启动依赖于uvicorn,还需要安装uvicorn pip install uvicorn -i https://mirr…

Spring之AOP的详细讲解

目录 一.SpringAOP是什么? 1.1理论知识点 1.2简单的AOP例子 二.SpringAOP的核心概念 2.1切点(Pointcut) 2.2通知(Advice) 2.3切⾯(Aspect) 2.4通知类型 2.5切⾯优先级 Order 2.6切点表达式 2.6.1 execution表达式 2.6.2annotati…

PHP 伪协议:使用 php://input 访问原始 POST 数据

文章目录 参考环境PHP 伪协议概念为什么需要 PHP 伪协议? php://input为什么需要 php://input?更灵活的数据处理减小性能压力 发送 POST 数据HackBarHackBar 插件的获取 $_POST打开 HackBar 插件通过 HackBar 插件发起 POST 请求 基操 enable_post_data_…

【ros】结果实时在线可视化

文章目录 一、前言二、订阅与发布三、回调四、可视化 4.1、初始化参数4.2、初始化图片 4.3、画结果 4.4、可视化结果 一、前言 感知与规划控制是无人驾驶算法重要算法,在交付测试阶段也最容易引起摩擦,这也是司空见惯的现象。有时候可能是接口对齐问题…

AI绘画与建筑大师共创出的作品,震惊了?!

在CAD制图盛行的今天,手绘依然是许多建筑大师首选的灵感记录方式。建筑大师西扎曾说过:草图能迅速的记录下他思维的瞬间,并再一次激发他更深入的思考。 看完这些建筑大师的手稿,不得不让人表示:这和医生处方手迹简直有…

【满满干货】聚合接口—自动化工具㊣

背景 在介绍接口自动化之前先给大家分享一下我所理解的“业务中台”的概念:业务中台是将企业的核心能力以数字化形式沉淀为各种服务中心,其目的是“提供企业能够快速,低成本创新的能力”。 例如公司内部的业务a、业务b同时有订单、登录等功…

企业网盘私有化部署和本地私有化部署的区别

在当今数据量激增的背景下,企业如何高效、安全地管理和传输大量数据成为了一个关键问题。企业网盘作为一种解决方案,其部署方式直接影响到数据的安全性、工作效率的提升以及运营成本的控制。私有化部署与本地化部署是两种主流的企业网盘部署策略&#xf…

C语言_文件操作

文件基础 什么是文件 文件是在计算机中以实现某种功能、或某个软件的部分功能为目的而定义的一个单位。磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分的)。 程序文件 …

【MATLAB源码-第33期】matlab基于遗传算法的多层编码柔性作业车间调度问题仿真

操作环境: MATLAB 2022a 1、算法描述 1. 遗传算法: 遗传算法是一种基于自然选择和遗传遗传学的优化算法。它模拟了生物进化的过程,通过对问题解的编码(通常以染色体或基因型的形式)、交叉、变异等操作来生成新的解。…

Coze 识别用户意图

文章目录 Coze 识别用户意图 Coze 识别用户意图 本文将通过 LLM 节点、Condition 节点和插件节点构建一个用于识别用户意图的工作流。 效果示例 本文构建的示例工作流概览如下。 在该工作流中: 使用 LLM 节点将用户输入数据分为 1(天气)、…

Flume实时读取目录文件到HDFS案例

【尚硅谷】大数据技术之Flume教程从入门到实战_哔哩哔哩_bilibili 目录 flume简介 flume案例 1、监控端口数据官方案例 2、实时读取目录文件到HDFS案例 flume简介 Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系…

【UE Niagara】烟雾特效

效果 步骤 1. 创建一个材质,这里命名为“M_Smoke” 设置混合模式为半透明,着色模型为无光照 连接如下节点 其中纹理采样节点所使用的纹理为引擎自带的“T_SmokeSubUV_8x8” 2. 新建一个Niagara发射器,模板使用“Empty”,这里命名…

MLeaksFinder报错

1.报错:FBClassStrongLayout.mm 文件:layoutCache[currentClass] ivars; 解决:替换为layoutCache[(id)currentClass] ivars; 2.编译正常但运行时出现crash indirect_symbol_bindings[i] cur->rebinding FBRetainCycleDetector iOS15 …

亚马逊运营必看!如何运用自养号测评获得买家评论转销量?

作为亚马逊卖家,相信大家对亚马逊的产品星级评分 (Rating) 都不陌生,这几颗亮眼的星星,不仅可以让你的Listing脱颖而出,获得足够多、足够高的产品评分,也是促使消费者下单的重要因素之一。 那么,亚马逊运营…

DepthFormer论文详解

摘要 本文旨在解决有监督单目深度估计的问题,我们从一项细致的试点研究开始,以证明远程相关性对于准确的深度估计至关重要。我们建议使用Transformer以有效地注意力机制对这种全局上下文进行建模。我们还采用一个额外的卷积分支来保留局部信息&#xff0…

NPU编译MultiScaleDeformableAttention

NPU对pytorch,想将检测模型在NPU上训练,存在编译MultiScaleDeformableAttention的需求。 然而,原dino模型https://github.com/IDEA-Research/DINO/tree/main/models/dino/ops/src 仅包含CPU版本和GPU版本: 是不是就真的无法解决…

2024/4/5—力扣—在排序数组中查找元素的第一个和最后一个位置

代码实现: 思路:二分法 方法一:分别查找左右侧边界 /*** Note: The returned array must be malloced, assume caller calls free().*/ int GetTargetFirstPosition(int *nums, int numsSize, int target) {int l 0, r numsSize - 1;while …