volatile与重排序

  使用关键字volatile可以禁止代码的重排序;

  在Java程序运行时,JIT(即使编译器)可以动态地改变程序代码运行地顺序;例如,有如下代码:

A代码-重耗时
B代码-轻耗时
C代码-重耗时
D代码-轻耗时

  

  在多线程环境下,JIT有可能进行代码重排序,重排序后地代码顺序有可能如下:

B代码-轻耗时
D代码-轻耗时
A代码-重耗时
C代码-重耗时

  这样做地主要原因是CPU流水线是同时执行这4个指令的,那么轻耗时的代码在很大程度上先执行完成,以让出CPU流水线给其他指令,所以代码重排序是为了追求更高的程序运行的效率;

  重排序发生在没有依赖关系时,例如,对于上面的A,B,C,D代码,B,C,D代码不依赖A代码的结果,C,D代码不依赖A,B代码的结果,D代码不依赖A,B,C代码的结果,这种情况下就会发生重排序,如果代码之间有依赖关系,则代码不会重排序;

  使用关键字volatile可以禁止代码重排序,例如,有如下代码:

A变量的操作
B变量的操作
volatile Z变量的操作
C变量的操作
D变量的操作

  

  那么会有4种情况发生:

  1. A,B可以重排序
  2. C,D可以重排
  3. A,B不可以重排到Z的后面
  4. C,D不可以重排到Z的前面

  

  换言之,变量Z是一个屏障,Z变量之前或之后不可以跨越Z变量,这就是屏障的作用,关键字synchronized具有同样的特性;

   1.关键字synchronized之前的代码不可以重排到synchronized之后

   2.关键字synchronized之后的代码不可以重排到synchronized之前

  使用双重检查锁实现多线程环境下的延迟加载单例模式

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() {

    }

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

        return singleton;
    }
}

  

  使用volatile修饰变量singleton使该变量在多个线程间达到可见性,另外也禁止了singleton =  new Singleton()的代码重排序,singleton = new Singleton()代码在内部分为3部分:

1.memory = allocate(); //分配对象的内存空间
2.ctorInstance(memory); //初始化对象
3.instance = memory; //设置instance指向刚分配的内存地址

  

  在一些JIT编译器上,这种指令重排是真实发生的;

1.memory = allocate(); //分配对象的内存空间
3.instance = memory; //设置instance指向刚分配的内存地址
2.ctorInstance(memory); //初始化对象

  所有线程在执行Java程序时都必须要遵守intra-thread semantics;intra-thread semantics保证重排序不会改变单线程内的程序结果;换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序;

      

  当线程A,线程B执行时,B线程访问instance所引用的对象,但这个对象没有被线程A初始化,线程B将看到一个还没有被初始化的对象;

  这里的A2和A3虽然重排序了,但Java内存模型的intra-thread semantics将确保A2一定会排在A4前面执行;因此,线程A的intra-thread semantics没有改变,但A2和A3的重排序,将会导致线程B判断instance实例不为空,线程B接下来将访问instance引用的对象(上图中线程B中的虚线),此时线程B访问到的是一个没有没有初始化的对象(没有进行赋值的对象),返回的是一个空的对象;

原文地址:https://www.cnblogs.com/coder-zyc/p/12650493.html