1 JMM(Java 内存模型)

1.1 基本概念

  • JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或规范,通过这组规范定义了程序中的访问方式。
  • JMM 同步规定
    • 线程解锁前,必须把共享变量的值刷新回主内存
    • 线程加锁前,必须读取主内存的最新值到自己的工作内存
    • 加锁解锁是同一把锁
  • 由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都在工作内存中进行。
  • 首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
  • 内存模型图

20190416211412.png

2 volatile 是 Java 虚拟机提供的轻量级的同步机制

2.1 可见性:

  • 在读取一个volatile变量之前,必须从主内存中拷贝最新值到工作内存
  • 在修改一个volatile变量之后,必须立刻把更新值刷新回主内存

    2.2 不保证原子性:

  • 模拟3个线程对一个共享变量v(volatile修饰)进行自增1

volatile不保证原子性-1558938293237.png

2.3 禁指令重排序:

  • 指令重排的介绍
    • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下 3 种
      • 编译器优化的重排
      • 指令并行的重排
      • 内存系统的重排
    • 单线程环境里面确保程序最终执行的结果和代码执行的结果一致
    • 处理器在进行重排序时必须考虑指令之间的数据依赖性
    • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
    • Java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的
  • 指令重排示例1

    1. public class ReSortSeqDemo {
    2. int a = 0;
    3. boolean flag = false;
    4. public void method01() {
    5. a = 1; // flag = true;
    6. // ----线程切换----
    7. flag = true; // a = 1;
    8. }
    9. public void method02() {
    10. if (flag) {
    11. a = a + 3;
    12. System.out.println("a = " + a);
    13. }
    14. }
    15. }

    如果两个线程分别执行method01 和 method02,如果线程 1 执行 method01 重排序了,然后切换的线程 2 执行 method02 ,就会出现不一样的结果。

  • 指令重排示例2
    在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:

    1. public class Singleton {
    2. private Singleton() { }
    3. private volatile static Singleton instance;
    4. public static Singleton getInstance(){
    5. if(instance==null){
    6. synchronized (Singleton.class){
    7. if(instance==null){
    8. instance = new Singleton();
    9. }
    10. }
    11. }
    12. return instance;
    13. }
    14. }

    这里为什么要加volatile了?我们先来分析一下不加volatile的情况,有问题的语句是这条:
    instance = new Singleton();
    这条语句实际上包含了三个操作:
    1.分配对象的内存空间;
    2.初始化对象;
    3.设置instance指向刚分配的内存地址。
    但由于存在重排序的问题,可能有以下的执行顺序:
    20181123113924_13549.png
    如果2和3进行了重排序的话,在线程A设置instance指向刚分配的内存地址后,发生线程切换,线程B进行判断if(instance==null)时就会为false,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错的。而用volatile修饰的话就可以禁止2和3操作重排序,从而避免这种情况。volatile包含禁止指令重排序的语义,其具有有序性。

  • volatile禁止指令重排序的深度分析
    volatile除了保证内存可见性,还有个作用是防止指令重排。寄存器和主存的隔离造成了数据的不一致,volatile的初衷是保证数据的强一致性,当赋值基本简单类型的时候,这种一致性很容易实现。但是赋值对象类型的时候,这种一致性分为强一致性和弱一致性,重排是弱一致性,而有序则是强一致性,volatile的目的是强一致性,所以最终它要求指令不得重排。