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

2024-04-09 11:36

本文主要是介绍并发编程——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的底层实现原理,讲了缓存一致协议,讲了有序性,讲了有序性的两大规则,讲了内存屏障。

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

这篇关于并发编程——5.JMM、可见性和有序性及volatile的底层实现原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/888041

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount