【1】JMM简介

① JMM

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

深入学习Java内存模型JMM

② 主内存(Main Memory)

主内存可以简单理解为计算机当中的内存(一部分),但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

③ 本地内存(Working Memory)

本地内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。为啥有本地内存这个概念?因为直接操作主内存太慢了

通过一系列内存读写的操作指令(JVM内存模型共定义了8种内存操作指令,以后会细讲),线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。

④ java内存模型相关变量

共享变量(实例域,静态域,数组元素)会受到内存模型影响。局部变量,方法定义参数等不会在线程间共享,所以他们不会有内存可见性问题,也不受内存模型影响


【2】指令重排序

理解重排序前这个概念前,我们先转换场景,从java内存模型走出来,来到硬件CPU这个维度。

① 基本概念:

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序(简单理解就是原本我们写的代码指令执行顺序应该是A→B→C,但是现在的CPU都是多核CPU,为了秀下优越,为了提高并行度,为了提高性能等,可能会出现指令顺序变为B→A→C等其他情况)。

当然CPU们也不是随便就去重排序,需要满足以下两个条件(遵循的规则):

  • 在单线程环境下不能改变程序运行的结果;
  • 存在数据依赖关系的不允许重排序

② 重排序分三类:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
深入学习Java内存模型JMM

那么重排序会遵循什么样的规则?


【3】重排序规则之as-if-serial

① as-if-serial语义的意思是:

不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。OK,这就相当于给CPU们定下规则。不要随便重排序。要满足我这个as-if-serial的前置条件,才能重排序。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

② as-if-serial语义把单线程程序保护了起来

遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。这里可以联想一下MySQL事务调度中的serializable,参考博文数据库事务中调度串行化、冲突可串行化、前趋图(优先图)

为了具体说明,请看下面计算圆面积的代码示例:

double pi  = 3.14;    //A

double r   = 1.0;     //B

double area = pi * r * r; //C

上面三个操作的数据依赖关系如下图所示:
深入学习Java内存模型JMM
如上图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到 A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。下图是该程序的两种执行顺序:
深入学习Java内存模型JMM

注意:as-if-serial只保证单线程环境,多线程环境下无效。那多线程,并发编程下怎么办?


③ 多线程下导致的问题及解决办法

上面的这些重排序都可能导致多线程程序出现内存可见性问题,JMM那么如何解决?

  • 对于编译器重排序,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。

  • 对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令Memory Barrier,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。


【4】什么是内存屏障

内存屏障(Memory Barrier)是一种CPU指令‘。

内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。

实际运用场景:

volatile便是基于内存屏障实现的。

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。这个指令就相当于一个内存屏障。具体表现为:

  • 当写一个volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。* 当读一个volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

从而保证了,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

【5】happens-before原则

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。

换句话说,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

happens-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

摘录一些happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。

  • 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。

  • volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

  • 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

happens-before与JMM的关系:
深入学习Java内存模型JMM


as-if-serial和happens-before小结

  • as-if-serial语义保证单线程内程序的执行结果不被改变

  • happens-before关系保证正确同步的多线程程序的执行结果不被改变。

其实都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。


【6】小结

重排序是多核CPU等为了性能进行的优化操作,但会导致可见性等问题。为了解决这些问题,所以JMM需要制定一些规则,不让其随意重排序。

as-if-serial只保证单线程环境的不可随意重排序,那么多线程下呢?

所以有了happens-before原则,其是JMM(JSR-133内存模型)的规范之一。

内存屏障是CPU指令。它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。

所以说,happens-before是JMM制定的最终目的,内存屏障则是实现happens-before的具体手段。

参考博文:
从内存可见性看Volatile、原子变量和CAS算法
多线程并发之volatile的底层实现原理

未完待续

相关文章: