详细介绍了JMM Java内存模型的概念、由来,以及happens-before原则的具体规则。
Java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的一组规范以及机制,本身是一种抽象的概念,并不真实存在。JMM的目标是通过控制主内存与每个线程的本地内存(工作内存)之间的交互,来为 Java 程序员提供内存可见性保证,以求多个线程能够正确的访问共享变量。Java是使用具体、改良的更好理解的happens-before原则来实现这一目标。
并发编程需要处理的两个关键问题是:线程之间如何通信 和 线程之间如何同步。
通信是指线程之间以何种机制来交换信息。
消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的 发送消息 来显式进行通信。在java中典型的消息传递方式就是wait()和notify()。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
Java 的并发采用的是 共享内存模型,线程之间的通信对程序员完全透明。同步的底层使用的是临界区对象,是指当使用某个线程访问共享资源时,必须使代码段独享该资源,不允许其他线程访问该资源。
“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象,它定义了共享内存系统中多线程程序读写操作行为的规则,通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。 它与处理器有关、与缓存有关、与并发有关、与编译器也有关。它解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下共享内存数据的正确性(一致性、原子性和有序性)。
不同架构的物理计算机可以有不一样的内存模型,Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型,并且它的内存访问操作与硬件的缓存访问操作具有很高的可比性。
Java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的一组规范以及机制 ,本身是一种抽象的概念,并不真实存在。它屏蔽掉了各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到内存访问的一致性(可见性、有序性、原子性),不必因为不同平台上的物理机的内存模型的差异,对各平台定制化开发程序。
原始的Java内存模型效率并不是很理想,因此Java1.5版本对其进行了重构,现在的Java8仍沿用了Java1.5的版本。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,比如一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数。
JMM定义了线程和主内存之间的抽象关系:所有的共享变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比,但只是一个抽象概念,物理上不存在),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(对于引用类型,可能拷贝对象的引用或者某个字段,但不会把这个对象整个拷贝一次),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(对于volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序的规定,所以看起来如同直接在内存中读写一般)。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。JMM通过控制主内存与每个线程的本地内存(工作内存)之间的交互,来为 Java 程序员提供内存可见性保证。
线程、主内存、工作内存三者的交互关系如图:
这里所讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
物理机高速缓存和主内存之间的交互有协议,同样的,Java也有关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,java内存模型中定义了8种操作来完成交互,虚拟机实现时必须保证这8种操作都是原子的、不可分割的(对于long和double类型的变量来说,load、store、read跟write在某些32位虚拟机平台上允许例外)。
JMM 在执行前面介绍的 8 种基本操作时,为了保证内存间数据一致性,JMM 中规定需要满足以下规则:
如上9种内存访问操作以及规则限定,再加上对volatile的一些特殊规定,就已经完全确定了java程序中哪些内存访问操作是在并发下安全的,以上的规则等效于happens-before(先行发生)原则。
从JMM设计者的角度,在设计JMM时,需要考虑两个关键因素:
由于这两个因素互相矛盾,所以JSR-133专家组在设计JMM时的核心目标就是找到一个好的平衡点:一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。JSR-133使用改良的happens-before原则来实现这一目标。 关于Java内存模型和顺序一致性内存模型、原始happens-before内存模型的关系,可以参考:JMM与顺序一致模型和Happens-Before模型的关系介绍。
如下一段代码:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
上面计算圆的面积的示例代码存在3个happens-before关系,如下。
- A happens-before B。
- B happens-before C。
- A happens-before C。
在3个happens-before关系中,2和3是必需的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分为了下面两类。
- 会改变程序执行结果的重排序。
- 不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略,如下。
- 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
- 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。
下面是JMM的设计示意图:
从上图可以看出两点:
从JDK 1.5开始,Java使用新的JSR-133内存模型。现在的Java内存模型是建立在happens-before(先行发生)内存模型而不是顺序一致性内存模型之上的,并且再此基础上,增强了一些。因为happens-before(先行发生)内存模型是一个弱约束的内存模型,在多线程竞争访问共享数据的时候,会导致不可预期的结果,这些结果有一些是java内存模型可以接受的,有一些是java内存模型不可以接受的。
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,阐述操作之间的内存可见性。 由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
《JSR-133:JavaTM内存模型与线程规范》对happens-before关系的定义如下:
上面的1)是JMM对程序员的承诺。 从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证,实际上并不一定执行顺序如预期!
上面的2)是JMM对编译器和处理器重排序的约束原则。 正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。 JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
as-if-serial 语义保证单线程内程序的执行结果不被改变,happens-before 关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial 语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before 关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按 happens-before 指定的顺序来执行的。
as-if-serial 语义和 happens-before 这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的效率。
单线程下的 happens-before,字节码的先后顺序天然包含 happens-before 关系:因为单线程内共享一份工作内存,不存在数据一致性的问题。
在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。
多线程下的 happens-before,多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程 1 更新执行操作 A 共享变量的值之后,线程 2 开始执行操作 B,此时操作 A 产生的结果对操作 B 不一定可见。也就不满足happens-before了。
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
注意:
如上图,一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。编译器和处理器遵从这一目标,从happens-before的定义我们可以看出,JMM同样遵从这一目标。
参考资料:
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!