jvm(三)指令重排 & 内存屏障 & 可见性 & volatile & happen before

参考文档:html

https://tech.meituan.com/java-memory-reordering.htmljava

http://0xffffff.org/2017/02/21/40-atomic-variable-mutex-and-memory-barrier/缓存

内存可见性:http://blog.csdn.net/ty_laurel/article/details/52403718app

1、什么是重排序函数

重排序分为2种优化

  • 编译期指令重排

经过调整代码中的指令顺序,在不改变代码语义的前提下,对变量访问进行优化。从而尽量的减小对寄存器的读取和存储,并充分复用寄存器。可是编译器对数据的依赖关系判断只能在单执行流内,没法判断其余执行流对竞争数据的依赖关系atom

  • CPU乱序执行(Out-of-Order Execution)

流水线(Pipeline)和乱序执行是现代CPU基本都具备的特性。机器指令在流水线中经历取指、译码、执行、访存、写回等操做。为了CPU的执行效率,流水线都是并行处理的,在不影响语义的状况下。处理器次序(Process Ordering,机器指令在CPU实际执行时的顺序)和程序次序(Program Ordering,程序代码的逻辑执行顺序)是容许不一致的,即知足As-if-Serial特性。显然,这里的不影响语义依旧只能是保证指令间的显式因果关系,没法保证隐式因果关系。即没法保证语义上不相关可是在程序逻辑上相关的操做序列按序执行spa

as-if-serial语义:.net

全部的动做均可觉得了优化而被重排序,可是必须保证它们重排序后的结果和程序代码自己的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义线程

       为保证as-if-serial语义,Java异常处理机制也会为重排序作一些特殊处理。例如在下面的代码中,y = 0 / 0可能会被重排序在x = 2以前执行,为了保证最终不致于输出x = 1的错误结果,JIT在重排序时会在catch语句中插入错误代偿代码,将x赋值为2,将程序恢复到发生异常时应有的状态。这种作法的确将异常捕捉的逻辑变得复杂了,可是JIT的优化的原则是,尽力优化正常运行下的代码逻辑,哪怕以catch块逻辑变得复杂为代价,毕竟,进入catch块内是一种“异常”状况的表现

public class Reordering {
    public static void main(String[] args) {
        int x, y;
        x = 1;
        try {
            x = 2;
            y = 0 / 0;    
        } catch (Exception e) {
        } finally {
            System.out.println("x = " + x);
        }
    }
}

重排序知足happen before原则

  1. 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操做happen—before(时间上)后执行的操做
  2. 管理锁定规则:一个unlock操做happen—before后面(时间上的前后顺序,下同)对同一个锁的lock操做
  3. volatile变量规则:对一个volatile变量的写操做happen—before后面对该变量的读操做
  4. 线程启动规则:Thread对象的start()方法happen—before此线程的每个动做
  5. 线程终止规则:线程的全部操做都happen—before对此线程的终止检测,能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行
  6. 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始
  8. 传递性:若是操做A happen—before操做B,操做B happen—before操做C,那么能够得出A happen—before操做C

2、什么是内存可见性

可见性:一个线程对共享变量值的修改,可以及时地被其余线程看到
共享变量:若是一个变量在多个线程的工做内存中都存在副本,那么这个变量就是这几个线程的共享变量

Java内存模型(JMM)
Java内存模型(Java Memory Model)描述了Java程序中各类变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
    全部的变量都存储在主内存中。每一个线程都有本身独立的工做内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝),如图

两条规定:

  • 线程对共享变量的全部操做都必须在本身的工做内存中进行,不能直接从主内存中读取
  • 不一样线程之间没法直接访问其余线程工做内存中的变量,线程间变量值的传递须要经过主内存来完成。

在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并非实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。这致使在同一个时间点,各CPU所看到同一内存地址的数据的值多是不一致

 

如何实现内存可见性:

要实现共享变量的可见性,必须保证两点

  • 线程修改后的共享变量值可以及时从工做内存中刷新到主内存中
  • 其余线程可以及时把共享变量的最新值从主内存更新到本身的工做内存中

1)synchronized实现可见性

 synchronized可以实现:
    原子性(同步)
    可见性
JMM关于synchronized的两条规定:

    • 线程解锁前,必须把共享变量的最新值刷新到主内存中
    • 线程加锁时,将清空工做内存中共享变量的值,从而使用共享变量时须要从主存中从新读取最新的值

线程解锁前对共享变量的修改在下次加锁时对其余线程可见
线程执行互斥代码的过程

    1. 得到互斥锁
    2. 清空工做内存
    3. 从主内存拷贝变量的最新副本到工做内存
    4. 执行代码
    5. 将更改后的共享变量的值刷新到主内存中
    6. 释放互斥锁

 2)volatile实现可见性

volatile关键字

              可以保证volatile变量的可见性

             不能保证volatile变量复合操做的原子

       volatile如何实现内存的可见性:

    • 深刻来讲:经过加入内存屏障和禁止重排序优化来实现的

               在每一个volatile写操做前插入StoreStore屏障,在写操做后插入StoreLoad屏障
               在每一个volatile读操做前插入LoadLoad屏障,在读操做后插入LoadStore屏障

    • 通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存。这样任什么时候刻,不一样的线程总能看到该变量的最新值。

线程写volatile变量的过程:

    1. 改变线程工做内存中volatile变量副本的值
    2. 将改变后的副本的值从工做内存刷新到主内存

线程读volatile变量的过程:

    1. 从主内存中读取volatile变量的最新值到线程的工做内存中
    2. 从工做内存中读取volatile变量的副本

synchronized vs volatile

  • volatile不须要加锁,比synchronized更轻量级,不会阻塞线程
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,没法保证原子性

 

3、内存屏障

内存屏障的做用:

  • 防止指令之间的重排序
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

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

  • 对于Load Barrier来讲,在指令前插入Load Barrier,可让高速缓存中的数据失效,强制重新从主内存加载数据
  • 对于Store Barrier来讲,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其余线程可见

java内存屏障:

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

final语义中的内存屏障:

  • 新建对象过程当中,构造体中对final域的初始化写入和这个对象赋值给其余引用变量,这两个操做不能重排序
  • 初次读包含final域的对象引用和读取这个final域,这两个操做不能重排序(先赋值引用,再调用final值)

4、优化屏障

避免编译器的重排序优化操做,保证编译程序时在优化屏障以前的指令不会在优化屏障以后执行。这就保证了编译时期的优化不会影响到实际代码逻辑顺序

优化屏障告知编译器:    内存信息已经修改,屏障后的寄存器的值必须从内存中从新获取    必须按照代码顺序产生汇编代码,不得越过屏障

相关文章
相关标签/搜索