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域已经正确完成初始化,对其他线程可见。
-
有序性:针对指令重排,java提供了volatile和synchronized,但是实现方式是不同的
- volatile本身禁止指令重排序,
- synchronized像是把多线程的环境变为了单线程的环境,并行变串行,指令重排必须保证串行语义的一致性。
③“天然的”先行发生原则 happens-before
happens-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.
如果两个操作的关系无法从上述规则中推倒出来,则虚拟机可以对它们随意重排序。衡量并发安全问题,应该以先行发生原则为准。