深入理解jvm-java内存模型(结合volatile)_volatile jvm-程序员宅基地

技术标签: jvm  java  多线程  

1. MESI结构

简述:为了解决CPU访问主内存的速度低下问题,存在以下的CPU与主内存交互模型
在这里插入图片描述

而为了保证CPU的缓存一致性,一般的主流方法有以下两种:

  1. 总线加锁,这种方式会阻塞CPU对其他组件的访问,略过。
  2. 缓存一致性协议。如下图。
    在这里插入图片描述
    内存间交互原子操作:
  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

其中 如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。


其操作根本为:
1)读取操作,不做任何处理,只将Cache的数据读取到寄存器。
2)写入操作,发出信号通知其他CPU该变量的Cache line已经无效,其他CPU在CPU缓存中读取这个变量时,需要到主内存进行读取。

2. JMM(Java内存模型)

在这里插入图片描述

  • 所有的变量存储在主内存,每个线程都可以访问
  • 每条线程都有自己的工作内存,为本地内存
  • 线程的工作内存保存了该线程所使用变量的主内存副本
  • 线程对变量的所有操作必须在工作内存中进行,不得直接操作主内存
  • 线程间变量的传递必须由主内存来交互完成

注意JMM只是一个抽象的概念

2.1 JMM与原子性

Java中对基本数据类型与引用类型的读取与赋值操作都是原子性的。
例子:
x = 1;原子操作;
y=x;非原子操作;存在从主内存读x 再赋值y 再将y刷新到主内存中。
y++ ; 非原子操作;y++ 同理于y= y+1

2.2 JMM与可见性

Java提供了三种方式来保证可见性:

  1. volatile,下面会介绍
  2. synchronized,在锁释放之前,会将变量刷新到主内存中
  3. JUC lock 同synchronized

2.3 JMM与有序性

Java提供了三种方式来保证有序性:

  1. volatile
  2. synchronized
  3. JUC lock

其他天然的有序性见Happens-before规则。

3. volatile理解

被volatile修饰的变量有以下属性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

  • 原子性:对任意单个volatile变量的读/写具有原子性,但是类似于volatile ++ 这种复合操作不具有原子性。(例如long,double)

  • 禁止对指令的重排序

重排序 (也可以理解写后读,读后写,写后写)

什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。

一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器
    可以改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为MemoryFence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

例子1:
在这里插入图片描述
例子2:
在这里插入图片描述
如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生。

还有一个例子:
在这里插入图片描述

可能会出现i=0,j=0 情况

原因:
这里线程one和线程two可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x=y=0的结果。
在这里插入图片描述


如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
Lock前缀触发效果
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
3)确保指令重排序不会将后面的代码排到内存屏障之前(同前不会排到后面)

volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

4.针对于64位基本类型long和double

如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的代表了“半个变量”的数值。不过这种读取到“半个变量”的情况是非常罕见的

5. 先行发生原则(Happens-Before)

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证

一个操作“时间上的先发生”不代表这个操作会是“先行发生”。那如果一个操作“先行发生”,是否就能推导出这个操作必定是“时间上的先发生”呢?很遗憾,这个推论也是不成立的。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the f irst is visible to and ordered before the second)。

时间先后顺序与先行发生原则之间基本没有因果关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准。

i = 1;       //线程A执行
j = i ;      //线程B执行

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i)。
那么可以确定线程B执行后j = 1 一定成立。
如果他们不存在happens-before原则,那么j = 1 不一定成立。

(即使代码是先执行j=1,然后执行j=i,也不一定j=1,主要看是否符合happens-before)


下面是Java内存模型下一些“天然的”先行发生关系

  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。

  • ·线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。

  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操

基于volatile的happens-before

在这里插入图片描述
1)根据程序次序规则,1 happens-before 2;3 happens-before 4。

2)根据volatile规则,2 happens-before 3。

3)根据happens-before的传递性规则,1 happens-before 4。

6. 基于volatile的内存语义

  • 写语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 读语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile内存语义的实现(禁止指令重排序的实现)

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

在这里插入图片描述
例子:
在这里插入图片描述
在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/littlewhitevg/article/details/105568194

智能推荐

启动mysql配置文件的启动方式_mysql mysql.conf force 启动-程序员宅基地

文章浏览阅读3k次。配置文件加载问题(查看自己使用的哪个配置文件)MySQL 配置文件加载位置与顺序: 1./usr/local/mysql/bin/mysqld –verbose –help >help.txt 2>&1 (/usr/local/mysql/bin/mysqld –verbose –help 这个命令生成所有mysqld选项和可配置变量的列表 然后重定向到help.txt,标准错误也_mysql mysql.conf force 启动

SpringCloud十一、虚拟机搭建eureka集群、在Linux安装jdk1.8、bash: /usr/java/jdk1.8.0_11/bin/java: cannot execute bina_/usr/jdk1.8.0_11/bin/java -javaagent:/usr/local/in-程序员宅基地

文章浏览阅读676次。用虚拟机搭建springcloud的eureka集群①借鉴电商六十、Nginx集群的虚拟机搭建(主分发器一台、备分发器两台)(克隆centos虚拟机文件,进度条卡了,按F12,卡在了starting atd [ok])。搭建虚拟机。先不安装nginx。只安装JDK1.8。借鉴 电商四、centos系统安装jdk和zookeeper或 一、搭建CentOS 6.4集群 ..._/usr/jdk1.8.0_11/bin/java -javaagent:/usr/local/intellij/lib/idea_rt.jar=472

基于Android的BLE通信软件_ble02可以和软件通信吗-程序员宅基地

文章浏览阅读685次,点赞4次,收藏9次。前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到教程。人工智能编程入门博客实现目标自己编写基于Qt的Android软件,用于实现手机与TB-02-kit模块进行数据通讯。Android软件发送的数据,经TB-02-kit模块转发至串口助手中输出;串口助手发送的数据可以在Android软件中显示,进而实现BLE的数据双向通信。所需工具及环境TB-02-kit模块Qt Creator 4.10.1Qt 5.13.1XCOM V2.0 串口助手A_ble02可以和软件通信吗

数字IC常考题(单选、多选、编程)_一个由两个相反类型的锁存器组成的寄存器(如下图所示),驱动端d由一个反相 器所驱-程序员宅基地

文章浏览阅读1.7w次,点赞15次,收藏176次。参考资料FPGA、数字IC系列(1)——乐鑫科技2021数字IC提前批笔试 - 知乎 (zhihu.com)FPGA/数字IC秋招笔试面试002——FPGA设计的面积优化和速度优化(2022届) - 知乎 (zhihu.com)IC/FPGA系统设计的速度和面积优化_Arist.-程序员宅基地_面积优化和速度优化一、单选题关于跨时钟域电路的设计,以下说法正确的是:A: 信号经两级D触发器同步后即可进行跨时钟域传递B: 跨时钟域电路存在亚稳态风险,最好避免使用C: 跨时钟域电路.._一个由两个相反类型的锁存器组成的寄存器(如下图所示),驱动端d由一个反相 器所驱

vue-cli(vue脚手架)超详细教程_vue cli模式教程-程序员宅基地

文章浏览阅读568次。目录前言1.安装vue-cli2.用vue-cli来构建项目3.启动项目4.vue-cli的webpack配置分析5.打包上线前言都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到陌生,重要的是思路的变换,以前用jq随便拿全局变量和修改dom的锤子不能用了,vue只用关心数据本身,不用再频繁繁琐的操作dom,注册事件、监听事件、取消事件。。。。(确实很烦)。vue的官方文档还是不错的,由浅到深,如果不使用构建工具确实用的_vue cli模式教程

python中文件读写open file read write等操作函数_filereadwrite-程序员宅基地

文章浏览阅读2.4k次。1. 文件对象的操作使用open()或者file()函数打开文件。使用file.read()读取文件。使用file.readline()读取文件的一行相关信息。使用file.write()进行写入文件。使用file.writelines(seq)向文件写入字符串序列seq。使用file.close()关闭文件。使用file.tell()返回当前在文件中的位置。使用file.seek..._filereadwrite

随便推点

进程管理_什么是程序, 什么是进程?进程的调度策略有几种方式?-程序员宅基地

文章浏览阅读1.7w次,点赞77次,收藏355次。进程管理_什么是程序, 什么是进程?进程的调度策略有几种方式?

笔记 ~ 第四章 - 4.3 视图、审计、数据加密及其他安全保护_如何理解视图对机密的数据提供安全保护-程序员宅基地

文章浏览阅读964次。๐•ᴗ•๐1. 视图机制2. 审计(Audit)3. 数据加密4. 其他安全性保护_如何理解视图对机密的数据提供安全保护

图像处理——matlab人脸识别(1)-程序员宅基地

文章浏览阅读2.1w次,点赞28次,收藏375次。目录一、前言二、相关程序(一)主函数(二)图库生成函数(三)图库图像命名函数(四)待识别图库生成函数(五)待识别图库命名函数(六)图像数据导入函数(七)PCA简单主成分分析函数(八)图像匹配函数三、识别效果一、前言近期,要做一个人脸识别的课题,于是在前人的基础上做了一些。对于图像处理我还属于初学阶段,在人脸识别算法上没有采用很高级的算法。参考的文章:利用MATLAB截取图片某个区域_蓝天萝卜-程序员宅基地Matlab实现人脸识别_王小壮的博_matlab人脸识别

二十三.激光和惯导LIO-SLAM框架学习之LIO-SAM项目工程代码介绍---基础知识_sc_lio_sam-程序员宅基地

文章浏览阅读3.3k次。专栏系列文章如下:一:Tixiao Shan最新力作LVI-SAM(Lio-SAM+Vins-Mono),基于视觉-激光-惯导里程计的SLAM框架,环境搭建和跑通过程_goldqiu的博客-程序员宅基地二.激光SLAM框架学习之A-LOAM框架---介绍及其演示_goldqiu的博客-程序员宅基地三.激光SLAM框架学习之A-LOAM框架---项目工程代码介绍---1.项目文件介绍(除主要源码部分)_goldqiu的博客-程序员宅基地四.激光SLAM框架学习之A-LOAM框架---项目工.._sc_lio_sam

android调用天地图,天地图嵌入到Android手机中-程序员宅基地

文章浏览阅读327次。该楼层疑似违规已被系统折叠隐藏此楼查看此楼3.2 使用步骤1) 将 API 文件 tiandituapi.jar 拷贝到工程根目录下,并在工程属性->JavaBuild Path->Libraries 中选择“Add External JARs”, tiandituapi.jar,确定后返回,这样您就可以在您的程序中使用 API 了。2) 需要在 Manifest 中添加如下访问权限..._android原生打开天地图

PMP第7章:成本基准易错习题和知识点汇总_什么过程输出成本基线-程序员宅基地

文章浏览阅读820次。PMP第7章:成本基准易错习题和知识点汇总1.成本基准是下列哪个过程的输出?A:规划成本管理B:估算成本C:制定预算正确答案D:控制成本答案解析: 成本基准是“制定预算“过程的输出。”正确答案:C以下哪项是估算成本过程的输入?A:工作分解结构B:成本基准C:合同D:资源日历答案解析: “工作分解结构属于范围基准,指明了项目全部可交付成果及其各组成部分之间的相互关系“,是估算成本过程的输入。正确答案:A3.通过项目状态报告,项目挣值(EV)为0.6,计划价值(PV)为0.1,通过_什么过程输出成本基线

推荐文章

热门文章

相关标签