前言

Java内存模型(Java Memory Model,简称JMM),即Java虚拟机定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能够达到一致的内存访问效果的内存模型。本篇文章大致涉及到五个要点:Java内存模型的基础,主要介绍JMM抽象结构;Java内存模型中内存屏障;Java内存模型中的重排序;happens-before原则;顺序一致性内存模型。还有与JMM相关的三个同步原语(synchronized,volatile,final)将另分三篇文章介绍。

1.Java内存模型的抽象结构

在java中,共享变量是指所有存储在堆内存中的实例字段,静态字段和数组对象元素,因为堆内存是所有线程共享的数据区。而局部变量,方法定义参数,异常处理参数不会在线程之间共享,它们不存在内存可见性问题,也不会受到Java内存模型的影响。

Java内存模型决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,Java内存模型定义了线程与主内存之间的抽象关系:线程之间的共享变量存储主内存中,每个线程都有一个私有的本地内存,也叫工作内存,本地内存存储了该线程需要读/写的共享变量的副本。本地内存是JMM的一个抽象的概念,其实并不真实存在。Java内存模型的抽象示意图如下:

JMM抽象结构.jpg

从上图来看,如果线程A和线程B之间要通信的话,必须要经历下面的两个过程:

1.线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2.线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图说明以上两个过程:

线程通信过程

如上图:假设初始时,X的值为0,首先线程A要先从主内存中读取共享变量x的值,并将其副本存储在自己的本地内存。接着线程A要把共享变量x的值更新为1,也就是先把本地内存中的x的副本的值更新为1,然后再把本地内存中刚更新过的共享变量刷新到主内存,此时主内存中共享变量x的值为1。然后线程A向线程B发送通知:哥们儿,我已更新了共享变量的值。

随后,线程B接收到线程A发送的通知,也从主内存中读取共享变量x的值,并将其副本存储在自己的本地内存,接着线程B也要修改共享变量的值,先将本地内存B中的副本x修改为2,再将本地内存中的x的值刷新到主内存,此时主内存中共享变量x的值就被更新为了2。

从整体上来看,上述的两个过程实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

2.Java内存模型的内存屏障

为了保证内存的可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令类禁止特定类型的处理器重排序,java内存模型(JMM)把内存屏障指令分为4类:

  • LoadLoad(Load1,LoadLoad,Load2):确保load1数据的装载先于load2及所有后序装载指令的装载。

  • LoadStore(Load1,LoadStore,Store2):确保Load1数据的装载先于Store2及所有后序存储指令刷新内存。

  • StoreStore(Store1,StoreStore,Store2):确保Store1数据刷新内存先于Store2及所有后序存储指令刷新内存。

  • StoreLoad(Store1,StoreLoad,Load2):确保Store1数据刷新内存先于Load2及所有后序装载指令的装载。该屏蔽指令会使该屏蔽之前的所有内存访问指令执行完成后才执行屏蔽之后的内存访问指令。并且这个指令是一个全能的指令,同时具备以上三个内存屏蔽指令的功能。

3.Java内存模型中的重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重排序的一种手段。重排序分为3种类型:

  1. 编译器优化重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓冲和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从java源代码到最终实际执行的指令序列,会经历下面3种重排序:

从源码到最终执行的指令序列示意图.jpg

上述1属于编译器重排序,编译器将java源码编译成字节码时进行一次重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成字节码指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

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

3.1 数据依赖性

如果两个操作访问同一个共享变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分为以下3种类型:

名称 代码示例 说明
写后读 a= 1 ; b = a 写一个变量之后,再读这个位置的变量值
写后写 a = 1 ; a = 2 写一个变量之后,再继续写这个内存位置的变量
读后写 a = b ; b = 1 读一个变量之后,再写刚读的这个变量

上面的3种情况,只要重排序两个操作的执行顺序,程序的结果就可能发生改变。

上面介绍过,编译器和处理器可能会对操作进行重排序。但是编译器和处理器进行重排序时会遵循数据依赖性规则,只要两个操作之间具有数据依赖性,那么编译器和处理器就不会对这两个操作进行重排序,编译器和处理器重排序的原则上是不改变程序的执行结果,从而提高程序执行性能。

这里所说的数据依赖性规则仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同的处理器之间和不同的线程之间的数据依赖性是不被编译器和处理器考虑的。

3.2 as-if-serial语义

as-if-serial语义是指:不管怎么重排序,单线程的执行结果是不能被改变的。编译器和处理器都必须遵循as-if-serial语义。

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

as-if-serial语义把单线程程序给保护了起来,遵循as-if-serial语义的编译器和处理器共同为编写单线程程序的程序员创造了一个幻觉:单线程程序是按程序代码的先后顺序来执行的。as-if-serial语义使程序员在单线程下无需担心重排序会影响程序执行结果,也无需担心内存可见性问题。

3.3 重排序对多线程的影响

重排序会可能影响多线程程序的执行结果,请看下面的示例代码:

public class ReorderExample{     int a = 0;      boolean flag = false;           @Test     public void