Java

Java内存模型的几个灵魂拷问

2020-10-08  本文已影响0人  千淘萬漉

Java内存模型(Java Memory Model,JMM)JSR-1337制定的规范,定义程序中变量的访问规则,屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在跨平台上都能达到内存访问的一致性。

JMM 内存模型

首先要记住有三个组件,CPU、工作内存和主内存,访问顺序也是按照如下图箭头的顺序来的。

JMM

运作原理: 通常,当一个cpu需要读取主存时,他会将主存的内容读取到缓存中,将缓存中的内容读取到内部寄存器中,在寄存器中执行操作,当cpu需要将结果回写到主存中时,他会将内部寄存器的值刷新到缓存中,然后会在某个时间点将值刷新回主存。详细的指令还可以继续划分为(lock、unlock、read、load、use、assign、store、write)

JMM的同步操作

可见性、原子性和有序性

缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题,其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高程序性能。

1、可见性

多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。就如同下面这张图:

线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性

2、原子性

操作系统做任务切换,可以发生在任何一条CPU 指令执行完,而 CPU 指令并不是高级语言里的一条语句。比如 count+=1new Object()这些程序语句都是几条CPU指令,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方,有些时候需要在高级语言层面保证操作的原子性。

3、有序性

有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。在 Java 领域一个经典的案例就是利用双重检查创建单例对象,我们以为的 new 操作应该是:

实际上有可能会发生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如何解决可见性和有序性问题》

具体的规则如下,有兴趣的可以看看:

3、内存屏障

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。内存屏障可以被分为以下几种类型:

讲完了这个地方,有了内存相关的基础,就可以在下面一篇文章里开始对volatile关键字进行讲解了。

参考引用

1、JVM内存模型、指令重排、内存屏障概念解析
2、极客时间,Java内存模型:看Java如何解决可见性和有序性问题

上一篇 下一篇

猜你喜欢

热点阅读