Volatile03_Volatile禁止指令重排、Volatile应用

57 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情

3_Volatile禁止指令重排

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

 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

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

线程安全获得保证

工作内存与主内存同步延迟现象导致的可见性问题

  • 可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见

对于指令重排导致的可见性问题和有序性问题

  • 可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化

4_Volatile的应用

单例模式DCL代码

首先回顾一下,单线程下的单例模式代码

 /**
  * SingletonDemo(单例模式)
  * @create: 2020-03-10-16:40
  */
 public class SingletonDemo {
     private static SingletonDemo instance = null;
     private SingletonDemo () {
         System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
     public static SingletonDemo getInstance() {
         if(instance == null) {
             instance = new SingletonDemo();
        }
         return instance;
    }
     public static void main(String[] args) {
         // 这里的 == 是比较内存地址
         System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
         System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
         System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
         System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    }
 }

最后输出的结果

但是在多线程的环境下,我们的单例模式是否还是同一个对象了

从下面的结果我们可以看出,我们通过SingletonDemo.getInstance() 获取到的对象,并不是同一个,而是被下面几个线程都进行了创建,那么在多线程环境下,单例模式如何保证呢?

解决方法1

引入synchronized关键字

     public synchronized static SingletonDemo getInstance() {
         if(instance == null) {
             instance = new SingletonDemo();
        }
         return instance;
    }

输出结果

我们能够发现,通过引入Synchronized关键字,能够解决高并发环境下的单例模式问题

但是synchronized属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是为了保证数据一致性,而减低了并发性,因此采用的比较少

解决方法2

通过引入DCL Double Check Lock 双端检锁机制

就是在进来和出去的时候,进行检测

     public static SingletonDemo getInstance() {
         if(instance == null) {
             // 同步代码段的时候,进行检测
             synchronized (SingletonDemo.class) {
                 if(instance == null) {
                     instance = new SingletonDemo();
                }
            }
        }
         return instance;
    }

最后输出的结果为:

从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性

 private static volatile SingletonDemo instance = null;

最终代码

 /**
  * SingletonDemo(单例模式)
  * @create: 2020-03-10-16:40
  */
 public class SingletonDemo {
     private static volatile SingletonDemo instance = null;
     private SingletonDemo () {
         System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
     public static SingletonDemo getInstance() {
         if(instance == null) {
             // a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
             synchronized (SingletonDemo.class) //b
            { 
            //c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
                 if(instance == null) { 
                 // d 此时才开始初始化
                     instance = new SingletonDemo();
                }
            }
        }
         return instance;
    }
     public static void main(String[] args) {
 //       // 这里的 == 是比较内存地址
 //       System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
 //       System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
 //       System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
 //       System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
         for (int i = 0; i < 10; i++) {
             new Thread(() -> {
                 SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
 }