Java内存模型(Java Memory Model,JMM)JSR-1337制定的规范,定义程序中变量的访问规则,屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在跨平台上都能达到内存访问的一致性。
JMM 内存模型
首先要记住有三个组件,CPU、工作内存和主内存,访问顺序也是按照如下图箭头的顺序来的。
JMM- 处理器(cpu): 寄存器:每个cpu都包含一系列寄存器,他们是cpu的基础,寄存器执行的速度,远大于在主存上执行的速度
- cpu高速缓存:由于处理器与内存访问速度差距非常大,所以添加了读写速度尽可能接近处理器的高速缓存,来作为内存与处理器之间的缓冲,将数据读到缓存中,让运算快速进行,当运算结束,再从缓存同步到主存中,就无须等待缓慢的内存读写了。处理器访问缓存的速度快与访问主存的速度,但比访问内部寄存器的速度还是要慢点,每个cpu有一个cpu的缓存层,一个cpu含有多层缓存,,某一时刻,一个或者多个缓存行可能同时被读取到缓存取,也可能同时被刷新到主存中,同一时刻,可能存在多个操作,
- 主内存:一个计算机包含一个主存,所有cpu都可以访问主存,主存通常远大于cpu中的缓存,
运作原理: 通常,当一个cpu需要读取主存时,他会将主存的内容读取到缓存中,将缓存中的内容读取到内部寄存器中,在寄存器中执行操作,当cpu需要将结果回写到主存中时,他会将内部寄存器的值刷新到缓存中,然后会在某个时间点将值刷新回主存。详细的指令还可以继续划分为(lock、unlock、read、load、use、assign、store、write)
JMM的同步操作可见性、原子性和有序性
缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题,其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高程序性能。
1、可见性
多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。就如同下面这张图:
线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性2、原子性
操作系统做任务切换,可以发生在任何一条CPU 指令执行完,而 CPU 指令并不是高级语言里的一条语句。比如 count+=1
、new Object()
这些程序语句都是几条CPU指令,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方,有些时候需要在高级语言层面保证操作的原子性。
3、有序性
有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。在 Java 领域一个经典的案例就是利用双重检查创建单例对象,我们以为的 new 操作应该是:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象;
- 然后 M 的地址赋值给 instance 变量。
实际上有可能会发生2、3步骤的互换。所以此处的优化方案是对instance进行volatile语义声明,就可以禁止指令重排序,避免该情况发生。
重排序的相关问题
1、指令重排
指令重排:大多数现代处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器就可以大大提高执行效率。
2、happens-before的原则
不同硬件环境下指令重排序的规则不尽相同,虽然代码重排序,乱排,要守一定的规则——happens-before原则,不能翻译为“先行发生”,它真正要表达的是:前面一个操作的结果对后续操作是可见的。只要符合happens-before的原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以自己排序。
在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
—— 极客时间:《看Java如何解决可见性和有序性问题》
具体的规则如下,有兴趣的可以看看:
- 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
- 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
- volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
- 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
- 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
- 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
- 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
- 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C。
3、内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。内存屏障可以被分为以下几种类型:
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
讲完了这个地方,有了内存相关的基础,就可以在下面一篇文章里开始对volatile关键字进行讲解了。
参考引用
1、JVM内存模型、指令重排、内存屏障概念解析
2、极客时间,Java内存模型:看Java如何解决可见性和有序性问题
网友评论