Java内存模型(JMM)那些事
本文是库存文章,去年年底学习了慕课网的并发编程课程,今年年初看完了《深入理解Java虚拟机》这本书,但是很多内容忘得差不多了,打算写写博客回忆一下那些忘在脑后的知识点。
温故而知新
更多Java并发文章:https://www.cnblogs.com/hello-shf/category/1619780.html
一、现代计算机内存模型
随着技术的发展,CPU也在按照摩尔定律快速发展,而内存即主存(Main Memory)发展却十分缓慢,所以CPU与主存间产生了一种因发展速度带来的矛盾,CPU发展太快导致主存跟不上CPU的发展速度,所以出现了三级缓存(不一定都是三级,明白就行),一种比主存读写速度更高的存储,三级缓存的出现暂缓了这种矛盾。ok,再远古的架构我们就先不聊了,从三级缓存的CPU架构看看现代计算机的内存模型。
CPU的流行架构如上图所示,当CPU要load一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
在三级缓存架构中我们不难发现,L3 cache和主存是共享的,所以就存在数据一致性的保障,MESI缓存一致性协议就作用在这里。MESI协议规定了CPU从主存(或者三级缓存)加载或者写入数据的规则,保证了数据的强一致性。关于MESI协议和三级缓存详情请查阅另一篇博客。
二、Java内存模型—JMM
Java内存模型(Java Memory Model)即JMM是一个抽象的概念,JMM是一个抽象的概念,JMM是一个抽象的概念,并不是物理上的内存划分,重要事情说三遍。
Java内存模型(JMM)定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作规范。在硬件内存模型中,各种CPU架构的实现是不尽相同的,Java作为跨平台的语言,为了屏蔽底层硬件差异,定义了Java内存模型(JMM)。JMM作用于JVM和底层硬件之间,屏蔽了下游不同硬件模型带来的差异,为上游开发者提供了统一的使用接口。说了这么多其实就是想说明白JMM——JVM——硬件的关系。总之一句话,JMM是JVM的内存使用规范,是一个抽象的概念。
如上图在JMM中,内存划分为两个区域,线程本地内存,主内存。
本地内存:每个线程均有自己的本地内存(Local Memory,也称之为线程的工作内存),本地内存是线程独占的。
主内存:存储所有的变量。如果一个变量被多个线程使用(被多个线程load到线程的本地内存中),则该变量被称之为共享变量。
三、JVM对JMM的实现
依据JMM规范,Java内存模型将JVM分为两个部分线程栈(Thread Stack)和堆(Heap)。
线程栈:线程独占,对其他线程不可见。线程间通信或者共享变量需要通过Heap。但另外的线程拿到的也只是该变量的私有拷贝,线程之间不能共享变量本身。
堆:线程创建的对象都在堆区,不管该对象是哪个线程创建的,也不管该对象是成员变量还局部变量。(很好理解,栈是运算速度比较快的区域,对象一般相对较大,栈中只需要一个变量指向堆即可。)
一个误区
具体各种类型的变量或者对象在JVM中的内存划分不是本文重点,本文我们主要讨论JMM。
看上图,是不是有点疑惑,在JVM中内存不是被划分为Java栈,堆,方法区,本地方法栈程序计数器等区域吗?为什么两个图对应不上呢?原因很简单,原则上来说这两个图是没有关系的。
JMM和JVM不是一个层次的东西,勉强对应起来,主内存、工作内存从定义上来看,主内存主要对应于java堆中的示例数据部分,而工作内存则对应于虚拟机栈的部分区域,从更低层次来说,主内存就直接对应物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器中,因为程序运行过程中主要读写访问的是工作内存。感觉知乎上有一个不错的解释。
四、JMM主内存和本地内存交互操作
计算机硬件内存模型有缓存和主内存的交互协议MESI,同样JMM也规范了主内存和线程工作内存进行数据交换操作。一共包括如上图所示的8中操作,并且每个操作都是原子性的。
- lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定。该操作表示该线程独占锁定的变量。
- unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定。
- read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用。
- load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)。
- use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作。
- assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作。
- store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用。
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
JMM规定了以上8中操作需要按照如下规则进行
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
以上8中规则看着也是比较生涩的,其实如果你没看明白也没关系,其实这些规则就是保障数据同步的一些规则。不是很重要,重要的在后面的happens-before原则。
五、并发环境下JMM存在的问题
并发编程的三个特征:原子性,有序性,可见性
原子性(Atomicity):一个操作是不可中断的,要么全部执行成功要么全部执行失败。
可见性(Visibility):所有线程都能看到共享内存的最新状态。(一个线程修改了一个共享变量,其他线程能够立即看到该变量的最新值)
有序性(Ordering):即程序执行的顺序按照代码的先后顺序执行。
以上三个概念应该很容易理解,在此不做过多解释。
1,原子性
JMM保证了四章节中的8个操作是原子性的,Java语言本身对基本数据类型的变量的读取和赋值操作是原子性操作。(JVM不对double和long类型的变量做原子性保障,可能的原因是缓存行的大小导致的)
比如
x = 1;//
x ++;//
y = x;//
以上三行代码其实只有x = 1是原子性的,这行代码只是对x进行赋值。
x ++;不是原子操作,因为这行代码包含三个操作:加载x的值,执行 ++,然后写入新值。单个操作是原子性的,多个操作组合起来就不是原子性的了。尤其是在并发环境下,如果x变量是多个线程共享的,会导致线程安全性问题。
y = x;同理也不是原型操作,因为需要首先加载x变量,再赋值给y。
在并发环境下,为了保证原子性通常采用synchronized或者Lock对代码块加锁保证原子性。
2,可见性
在Java中提供了一个volatile关键字来保证可见性。当一个主内存中的共享变量被volatile关键字修饰时,一个线程对该变量的修改会被立即刷新(store)到主内存,保证其他线程看到的值一定是最新的。可以参考
JMM层面上volatile是通过load/store操作实现的可见性,当然我们也可以通过synchronized和Lock通过加锁将多线程进行同步也就是串行执行来保证共享变量的可见性。很好理解,当两个线程都需要操作一个共享变量,后到的线程需要等到先到的线程执行完才能继续执行,变相的保证了数据的可见性。
当然在可见性层面,加锁相对于volatile是比较重量级的一个操作。
3,有序性
happens-before原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于后面的操作。
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。(先释放锁,才能加锁)
volatile变量规则:对同一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于C,则A先行发生于操作C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程终结规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则:线程中所有的操作都先行发生于线程的终结检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
happens-before是干嘛的呢?我理解的happens-before原则就是一个对人来说显而易见的东西。但是程序并不能理解这么些东西。
happens-before与可见性
happens-before通过以上8中规则保证可见性,如果一个操作A happens-before 另一个操作B,那么操作A的结果是对操作B可见的。不难理解。
happens-before与重排序
两个操作如果存在happens-before关系,并不意味着一定是有序进行的,因为JVM存在指令重排优化,如果JVM认为两个操作重排序有利于性能提升并且重排序后的操作和未重排结果一致,将进行指令重排序。当然JVM层面的重排序发生于编译期,运行时的指令重排是处理器决定的。
Java语言通过volatile关键字通过向主内存加入内存屏障实现禁止指令重排。
如有错误的地方还请留言指正。
原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html
参考文献:
https://www.jianshu.com/p/8a58d8335270
http://ifeve.com/java-memory-model-6/
https://www.jianshu.com/p/76959115d486
《深入理解Java虚拟机》
《慕课网-java并发编程》
Java内存模型(JMM)那些事的更多相关文章
- Java内存模型JMM与可见性
Java内存模型JMM与可见性 标签(空格分隔): java 1 何为JMM JMM:通俗地讲,就是描述Java中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这 ...
- 多线程并发之java内存模型JMM
多线程概念的引入是人类又一次有效压寨计算机的体现,而且这也是非常有必要的,因为一般运算过程中涉及到数据的读取,例如从磁盘.其他系统.数据库等,CPU的运算速度与数据读取速度有一个严重的不平衡,期间如果 ...
- Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)
JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- 全面理解Java内存模型(JMM)及volatile关键字(转)
原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...
- 什么是Java内存模型(JMM)
什么是java内存模型 缓存一致性问题 在现代计算机中,因为CPU的运算速度远大于内存的读写速度,因此为了不让CPU在计算的时候因为实时读取内存数据而影响运算速度,CPU会加入一层缓存,在运算之前缓存 ...
- 对多线程java内存模型JMM
多线程概念的引入体现了人类重新有效压力寨计算机.这是非常有必要的,由于所涉及的读数据的过程中的一般操作,如从磁盘.其他系统.数据库等,CPU计算速度和数据读取速度已经严重失衡.假设印刷过程中一个线程将 ...
- 深入理解Java内存模型JMM与volatile关键字
深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...
- Java内存模型(JMM)详解
在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型.它们的答案是一致的:能够让我们更好的 ...
- Java并发编程:Java内存模型JMM
简介 Java内存模型英文叫做(Java Memory Model),简称为JMM.Java虚拟机规范试图定义一种Java内存模型来屏蔽掉各种硬件和系统的内存访问差异,实现平台无关性. CPU和缓存一 ...
随机推荐
- HashMap,ConcurrentHashMap原理。Collection(list,set,map集合区别)。和CAS
collection里面有什么子类?(list和set是实现了collection接口的.) List: 1.可以允许重复的对象(可重复,有序集合).2.可以插入多个null元素.3.常用的实现类有 ...
- VS+QT创建一个OpenCV应用
1.选择:文件->新建->项目->搜索“QT”->Qt GUI Application,输入工程名,下一步…进行新建. 选择依赖的模块:最基础的QtCore.QtGui.QtW ...
- 简写函数字面量(function literal)
如果函数的参数在函数体内只出现一次,则可以使用下划线代替: val f1 = (_: Int) + (_: Int) //等价于 val f2 = (x: Int, y: Int) => x + ...
- python接口自动化之fiddler使用(二)
1.快捷设置,自定义会话框,查看get和post请求 (1)鼠标放在#后面,右键 (2)选择Customize columns (3)选择Miscellaneous (4)选择RequestMetho ...
- Django 初试水(二)
这部分链接上一部分.将建立数据库,创建第一个模型,并主要关注 Django 提供的自动生成的管理页面. 打开 mysite/setting.py 文件.这包含了 Django 项目设置的 Python ...
- 搭建Hexo实现个人网站详细教程
全网最全小白搭建Hexo+Gitee/Coding/Github 全网最全小白搭建Hexo+Gitee/Coding/Github 本站内容已全部转移到https://www.myyuns.ltd,具 ...
- No Delegate set : lost message:libpng error: Not a PNG file
当出现这个问题时,是因为本来是jpg或其他格式的图片存成了png导致的.或者有的图片本来就是jpg的,Android Studio一编译,发现不是png才造成了这个问题.解决这个问题可以在Androi ...
- 通过属性选择器找元素,可以通过$(__).length是否为0来判断是否找到了元素
通过属性选择器找元素,可以通过$("").length是否为0来判断是否找到了元素. 为0的时候表示没有找到,其余则返回找到了多少个. 不能通过$("")是否为 ...
- C++-LUOGU2938- [USACO09FEB]股票市场Stock Market-[完全背包]
开O2,开O2,开O2 重要的事情说三遍 #include <set> #include <map> #include <cmath> #include <q ...
- AcWing 285. 没有上司的舞会
//f[u][0]是所有以u为根的子树中选择,并且不选u这个点的方案 //f[u][1]是所有以u为根的子树中选择,并且 选u这个点的方案 #include <cstring> #incl ...