JAVA内存模型
之前在高并发之缓存中介绍了CPU的缓存,提到了缓存一致性。在多线程并发编程中,由于CPU的指令重拍以及缓存的存在,导致我们无法保证正确的程序执行顺序,也无法保证共享变量的准确性。对于并发数据安全,主要就是要保证原子性,可见性,有序性。具体可见之前的文章: 高并发之缓存
下面图展示了从源代码到最后指令执行的经历的过程:
编译器优化会在单线程的情况下改变程序语句执行顺序;
指令级是处理器会近似并行执行指令,在没有数据依赖的情况下,可能会改变指令执行顺序;
Java内存模型机就是为了解决在多线程的情况下可能会引起程序结果不确定的问题,它底层主要通过限制指令重排以及增加内存屏障等方式实现。
对于Java内存模型(JMM)的定义,我觉得深入理解JAVA虚拟机说得很好:
Java虚拟机规范中试图定义一种Java内存模型 (Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。
JMM中也规定了主内存和工作内存,对应着CPU的内存和高速缓存,但它的概念仅仅是在JMM中定义的。
JMM保证了原子性、可见性、有序性。
原子性是保证变量的读取是原子操作;
可见性,即一个线程修改,其他线程立即可见。目前实现方式有volatile,final,synchronized;
有序性,就是在一个线程内执行是有序的。volatile禁止指令重排,
对于volatile具体介绍可以看我之前的写的文章: Java多线程共享变量同步机制
JMM中有一个最重要的概念叫做happens-before,其实在上面的文章中也有介绍,它主要包括:
- 程序顺序规则:同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
通过happens-before原则,JMM保证,在满足上述的几个原则的情况下,如果重排序会改变程序执行结果(包括单线程和多线程的情况),JMM会禁止重排序;如果不会影响结果,JMM不会强制禁止重排序。
原子操作:
lock:将一个变量标识为被一个线程独占状态
unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
read:将一个变量的值从主内存传输到工作内存中,以便随后的load操作
load:把read操作从主内存中得到的变量值放入工作内存的变量的副本中
use:把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
assign:把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
store:把工作内存中的一个变量的值传递给主内存,以便随后的write操作
write:把store操作从工作内存中得到的变量的值写到主内存中的变量
参考资料:
Java 虚拟机 2:Java内存模型-主内存与工作内存的交互协议
微信分享/微信扫码阅读