摘要:内存模型对内存模型的介绍对内存模型的结构图的线程之间的通信是通过共享内存的方式进行隐式通信,即线程把某状态写入主内存中的共享变量,线程读取的值,这样就完成了通信。
Java内存模型(JMM) 1.对内存模型的介绍 ①对Java内存模型的结构图
java的线程之间的通信是通过“共享内存”的方式进行隐式通信,即线程A把某状态写入主内存中的共享变量X,线程B读取X的值,这样就完成了通信。是一种隐式的通信方式。
一个线程的模型可以类比现在的CPU,一个CPU会具备高速缓存,来缓解CPU速度和内存IO速度的巨大差距,线程也是类似的,一个线程拥有其本地内存,相当于是用来缓存主内存中的值的。
也就是说,线程并不直接与主内存通信,而是线程先把主内存中的共享变量备份到私有的本地内存中,线程是使用本地内存中的值。
虚拟机,甚至硬件本身的优化措施,会优先将本地内存存储在寄存器和高速缓存。
②JMM的作用java的最大卖点是“一次编写,到处运行”,为了实现让java在各种平台上都能有一致的内存访问效果,java虚拟机规范必须定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问差异。
③JMM的特征java内存模型是围绕着如何处理原子性,可见性,有序性来建立的。
原子性:原子性指多个操作的组合要么一起执行完,要么全部不执行,这个很好理解,一个线程在执行一组操作的中途,不能被另一个线程插一脚,不然会造成数据错误,最经典就是 a++;操作,++ 操作符不是原子的,所以需要使用同步工具保证其原子性。
可见性:根据java内存模型的结构,各个线程都会从主内存备份一个变量的工作内存放在自己的工作内存作为缓存,可以提高效率,这样就造成了可见性问题,即一个线程修改了一个数据,如果一没有立即同步回主内存,二没有让其他使用这个数据的线程及时从主内存同步,则其他线程的数据是错误的。
有序性:编译器和处理器为了获得更高的效率,会对指令进行重排序,实际生成的字节码指令顺序或者处理器指令顺序并非是程序源代码中的顺序,这个在单线程的情况下问题不大,因为编译器和处理器会保证结果正确,但是多线程的环境下,因为线程之间很多时候需要协调,如果指令进行重排,会影响协调结果错乱,可以从一个经典的例子来说明,代码如下:
假设有两个线程A,B ,A线程先执行write方法,接下来B线程执行read()方法,write方法中的两个操作,并没有必要的顺序关系,在实际执行中,编译器或者处理器有权利进行重排序,先对flag赋值,然后对a赋值,巧了,B线程对flag的读取正好在A线程两个操作的中间,即B线程读取到了flag为true,但是a却还是0,造成了数据的错误。。
因此,JMM必须提供了一种机制来禁止类似的重排序,详见volatile的内存语义,其提供了对有序性的保证。
class OrderExample{ int a =0 ; boolean flag = false; public void write(){ a = 1; flag = true; } public void read(){ if(flag){ int i = a + 1; } } }④JMM的设计要求
我们可以把JMM看做是程序员和平台的中间人。程序员和平台需要谈判却==不直接交流==,而是通过JMM来传话。
先看双方的需要:
程序员的需求:程序员希望内存模型简单易懂,符合人类的直觉,所以渴望一个强内存模型
编译器和处理器的需要:编译器,处理器希望内存模型对自己的束缚越小越好,这样就可以做更多的优化来提高执行速度,编译器,处理器渴望弱内存模型。
所以JMM有两个设计需求,1.为程序员提供可见性保证,2.尽可能放松对编译器,处理器的限制。
所谓谈判,是一个妥协的过程,JMM给了程序员一些“先行发生原则happens-before”的保证,程序员的代码中的操作之间关系只要符合这些规则,那么平台不会随意对这些操作重排序,程序员根据这个保证,可以使编程更加容易和健壮,更符合人类的直觉。同时,JMM也放宽了对平台的限制,只要能保证那些“happens-before规则”,平台可以对操作进行重排序。
2.内存模型如何实现三个特性 ①主内存与工作内存之间的交互协议定义了一个变量如何从主内存中拷贝到工作内存(本地内存),如何从工作内存同步回主内存的实现细节。
JMM中定义了8中操作来完成以上工作,每种操作都是原子的,不可再分的(double,long类型的变量,load,store,read,write操作在某些平台上允许有例外)
命令 | 作用于何处的变量 | 作用描述 |
---|---|---|
lock | 主内存 | 把一个变量标识为一个线程独占的状态 |
unlock | 主内存 | 把一个变量从锁定状态释放出来,与lock对应 |
read | 主内存 | 把一个变量的值从主内存中传输到线程的工作内存,供load使用 |
load | 工作内存 | 把read操作得到的值放入到工作内存的变量副本中 |
use | 工作内存 | 把工作内存中的值传递给执行引擎 |
assign | 工作内存 | 把从执行引擎收到的值赋值给工作内存中的该变量 |
store | 工作内存 | 将工作内存中的该变量的值传送到主内存 |
write | 主内存 | 将store操作得到的值放入主内存的变量中 |
这些操作需要必须遵循的规定:
JMM规定read-load,store-write两对操作必须顺序执行,而且必须成对出现,但是不规定连续执行,
工作内存有状态的改变必须同步会主内存
不允许没有发生过任何assign的情况下把数据同步回主内存
一个新的变量只能在主内存中诞生,即use和store之前必须有对该变量的assign,load
一个变量在同一时刻只允许一个线程对其执行lock操作,但是允许该线程多次执行lock操作,对应的,unlock也必须执行相同的次数才能解锁。可重入锁
对一个变量执行lock操作,必须清空工作内存中此变量的值,在执行引擎使用该变量之前,重新执行load或assign。 synchronized也具备内存可见性
如果一个变量事先未被Lock锁定,那么不允许对其unlock操作,也不能unlock一个被其他线程锁定的变量。
对一个变量unlock操作之前,必须把此变量同步会主内存。也可服务于synchronized的可见性
8中内存访问以及上述的8个规定限制,加上volatile的写特殊规定,已经完全确定了java程序的那些内存访问操作是线程安全的。 以上的规定的一个等效判断原则就是 ==happens-before==。 ②三个特性的实现
JMM使用read,load,assign,use,store,write来访问基本数据,这些操作都是原子的,所以基本可以认为JMM对基本数据类型的访问是原子的
对于更大范围的原子性保证,JMM提供了lock和unlock来满足这种需求,lock和unlock并未直接提供给用户使用,但是可以通过更高级的字节码指令,monitorenter,monitorexit来隐式使用。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,具体的使用可以看synchronized原语的实现。
volatile变量提供的可见性:普通变量和volatile变量都是通过主内存作为线程间分享数据的渠道,不同的是,volatile变量能及时同步到主内存并且其他线程工作内存中该变量的值立即失效,需要重新从主内存中加载,普通变量不保证。
synchronized提供的可见性:实现方式和volatile有所不同,"八大规定"中说,unlock操作之前,必须先把变量同步到主内存中,而lock之前,必须清空工作内存中的值,重新从主内存中加载。这两条保证了synchronized具备内存可见性。
final提供的内存可见性:final的重排序规则明确了,只要正确构造一个对象,那么当线程获得这个对象的时候,其final域已经正确完成初始化,对其他线程可见。
volatile本身禁止指令重排序,
synchronized像是把多线程的环境变为了单线程的环境,并行变串行,指令重排必须保证串行语义的一致性。
③“天然的”先行发生原则 happens-beforehappens-before服务于三大原则中的有序性,Java程序中天然的(未使用volatile或者synchronized)有序性可以总结为一句话:
如果在本线程观察,所有操作都是有序的,如果在另一个线程中观察,所有操作都是无序的。
试想一下,如果编码过程中所有的操作都要使用volatile或者synchronized来保证有序性,那么将是多大的负担,程序的复杂性也会极大上升。所以java中提供了一些天然的先行发生原则,是指那些无需任何同步手段就天然具备顺序性的先行发生原则。是8个内存操作的规则的另一种表达。
程序次序规则:在一个线程内,按照控制流,前面的操作先于后面的操作,对后续操作可见。as-if-serial语义。
管程锁定规则:一个unlock操作必须先行发生于同一个锁的lock操作
volatile变量规则:对volatile的写操作必须happens-before后续对该变量的读操作。
线程启动规则:start()方法happens-before该线程的其他所有动作
线程终止规则:线程中所有的操作happens-before该线程的终止检测,如isAlive()方法。
线程中断规则:interrupt()方法happens-before对线程中断的检测
对象终结规则:一个对象初始化完成happens-before其finilize()方法的开始
传递性,A hannens-before B,B happens-before C,则A happens-before C.
如果两个操作的关系无法从上述规则中推倒出来,则虚拟机可以对它们随意重排序。衡量并发安全问题,应该以先行发生原则为准。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/69352.html
摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...
摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...
摘要:编译器,和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。正确同步的多线程程序的执行将具有顺序一致性程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。 前情提要 深入理解Java内存模型(六)——final 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内...
摘要:内存模型即,简称,其规范了虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,如何同步访问共享变量。内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。 Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,...
摘要:作为一个程序员,不了解内存模型就不能写出能够充分利用内存的代码。程序计数器是在电脑处理器中的一个寄存器,用来指示电脑下一步要运行的指令序列。在虚拟机中,本地方法栈和虚拟机栈是共用同一块内存的,不做具体区分。 作为一个 Java 程序员,不了解 Java 内存模型就不能写出能够充分利用内存的代码。本文通过对 Java 内存模型的介绍,让读者能够了解 Java 的内存的分配情况,适合 Ja...
阅读 2689·2021-11-08 13:16
阅读 2366·2021-10-18 13:30
阅读 2236·2021-09-27 13:35
阅读 1992·2019-08-30 15:55
阅读 2441·2019-08-30 13:22
阅读 575·2019-08-30 11:24
阅读 2073·2019-08-29 12:33
阅读 1812·2019-08-26 12:10