# JMM

# 0. 原子性、可见性、有序性

并发编程中引起非线程安全的主要原因是共享变量的原子性、可见性、有序性。

  • 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。

对共享变量操作不满足原子性,多线程访问会造成数据不一致。

  • 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

Java为了解决重排序、原子性、可见性问题,提供了语言级别的两个关键字synchronized和volatile。

volatile是Java内存模型提供的最轻量级的同步机制,它可以保证多线程操作共享变量的有序性以及内存可见性。

synchronized关键字则是一个互斥锁,可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

# 双重校验的例子:

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
    return instance;
    }
}

由于 instance = new Singleton()不是原子性,需要三个JVM指令,

  1. 分配内存
  2. 初始化
  3. 赋值

由于指令重排序,导致先赋值,后初始化,其他线程拿到不是null但没有初始化的instance。而将instance用volatile修饰可以禁止指令重排序,从而解决该问题。

# 1.概述

JAVA内存模型(Java Memory Model)简称JMM。

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

JMM的主要目标是定义程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。

共享变量主要指实例变量

JMM主要影响共享变量在主内存和工作内存的一致性问题。

JMM将内存分为两种:

  • 主内存(Main Memory)
  • 本地内存(Working Memory)或工作内存

每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

JMM对多线程对于共享变量的读写机制规定

  1. 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  2. 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量传递需要经过主内存同步
  3. 如何做数据同步以及什么时候数据同步。

本地内存是一个抽象概念,统一了缓存,缓冲区,寄存器等硬件以及编译器优化等。

# happens before

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

  • 1 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
  • 2 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
  • 3 volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
  • 4 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
  • 线程启动规则:在线程上调用start()方法,必须在该线程执行任何操作之前执行。
  • 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已结束之前执行。
  • 终结器规则:对象的构造函数必须在启动该对象终结器之前执行完成。
  • 中断规则:对线程 interrupt方法的调用,happens-before被中断线程的代码检测到中断事件的发生。

两个操作之间存在happens-before关系,并不意味着一定要按照 happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before关系来执行的结果一致,那么这种重排序并不非法。

# as-if-serial

as-if-serial语义的是:所有的操作均可以为了优化而被重排序,但是必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial语义。

注意,as-if-serial 只保证单线程环境,多线程环境下无效。

# final

对于final域,编译器和处理器要遵守两个重排序规则。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

# 2. 指令重排序

编译器或处理器为了提高处理性能会对指令进行重排序。

满足条件:

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

重排序分类:

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

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

4、内存屏障

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

  • 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
  • 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。 内存屏障有两个作用:

阻止屏障两侧的指令重排序; 强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效。

# 8种内存指令

  • lock:标识为独占状态
  • unlock:释放独占状态
  • read:主内存到工作内存
  • load:加载到工作内存的变量副本
  • use:传递给执行引擎执行指令使用
  • assign:从执行引擎赋值到工作内存
  • store:工作内存到主内存
  • write:放入主内存变量

内存模型能保证这8个指令是原子性的,但没有办法保证这些指令组合在一起的原子性。

内存模型规定,把一个变量从主内存复制到工作内存中,就要顺序的执行read和load操作,如果要把变量同步回主内存,就要顺序的执行store和write操作。

# 后序

JMM对开发者影响主要体现在多线程编程模块,可以理解为了屏蔽CPU缓存对多线程的影响设计的模型。

线程间通信的一种方法就是共享内存,共享内存就是不同线程访问同一内存进行信息共享。理解JMM才能理解内存可见性,正确进行线程间通信。

JMM规范了线程对内存的读写操作,从而保证不同环境下指令执行的正确性。