1、volatile概念

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

    volatile三个特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

2、volatile禁止指令重排

(1)指令重排有序性:

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令排重,一般分为以下三种:

java volatile指令重排 java volatile 指令重排序_多线程

单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。

处理器在进行指令重排序时必须考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。

指令重排案例分析one:

public void mySort() {
        int x = 11; // 语句1
        int y = 12; // 语句2
        x = x + 5; // 语句3
        y = x * x; // 语句4
    }
// 指令重排之后,代码执行顺序有可能是以下几种可能?
    // 语句1 -> 语句2 -> 语句3 -> 语句4
    // 语句1 -> 语句3 -> 语句2 -> 语句4
    // 语句2 -> 语句1 -> 语句3 -> 语句4
    // 问题:请问语句4可以重排后变为第1条吗?
    // 不能,因为处理器在指令重排时必须考虑指令之间数据依赖性。

指令重排案例分析two:

java volatile指令重排 java volatile 指令重排序_java volatile指令重排_02

指令重排案例分析three: 

public class BanCommandReSortSeq {
    int a = 0; 
    boolean flag = false; 
 
    public void methodOne() {
        a = 1;  // 语句1
        flag = true;    // 语句2
 
        // methodOne发生指令重排,程序执行顺序可能如下:
        // flag = true;    // 语句2
        // a = 1;  // 语句1
    }
 
    public void methodTwo() {
        if (flag) {
            a = a + 5;  // 语句3
        }
        System.out.println("methodTwo ret a = " + a);
    }
    // 多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。
    // 多线程交替调用会出现如下场景:
    // 线程1调用methodOne,如果此时编译器进行指令重排
    // methodOne代码执行顺序变为:语句2(flag=true) -> 语句1(a=5)
    // 线程2调用methodTwo,由于flag=true,如果此时语句1还没有执行(语句2 -> 语句3 -> 语句1 ),那么执行语句3的时候a的初始值=0
    // 所以最终a的返回结果可能为 a = 0 + 5 = 5,而不是我们认为的a = 1 + 5 = 6;
}

(2)禁止指令重排底层原理:

     volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

    先了解下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作执行的顺序性
  • 保证某些变量的内存可见性(利用该特性实现volatile内存可见性)

volatile实现禁止指令重排优化底层原理:

    由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排,也就是说通过插入内存屏障,就能禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

左边:写操作场景:先LoadStore指令,后LoadLoad指令。

右边:读操作场景:先LoadLoad指令,后LoadStore指令。

java volatile指令重排 java volatile 指令重排序_java volatile指令重排_03

 

3、volatile使用场景

单例模式(DCL-Double Check Lock双端检锁机制)

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

public class SingletonDemo {
    private static SingletonDemo singletonDemo=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
    }
    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static SingletonDemo getInstance(){
        if (singletonDemo==null){
            synchronized (SingletonDemo.class){
                 if (singletonDemo==null){
                     singletonDemo=new SingletonDemo();
                 }
            }
        }
        return singletonDemo;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i+1)).start();
        }
    }
}

这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步

memory = allocate();     //1.分配内存
instance(memory);	 //2.初始化对象
instance = memory;	 //3.设置引用地址

其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

解决的方法就是对singletondemo对象添加上volatile关键字,禁止指令重排。