volatile双重检查锁定与延迟初始化

一、基本概念:

  1、volatile是轻量级的synchronized,在多核处理器开发中保证了共享变量的“可见性”。可见性的意思是,当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。

       2、volatile在修饰共享变量进行写操作时,在多核处理器下会引发两件事情:

    1)将当前处理器缓存行的数据写回到系统内存。

    2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

  3、在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,

    当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。当处理器对这个数据进行修改操作的时候,会重新从系统内存

    中把数据读到处理器缓存里。

  4、锁的happens-before规则保证释放锁和获取锁两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后

    写入。即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,

    这些操作整体上不具有原子性。简而言之:

    1)可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后写入。

            2)原子性。对任意一个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

  5、volatile写-读内存语意:

    写语意:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

    读语意:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

二、volatile在双重检查锁定与延迟初始化中的应用

  在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双重检查锁定是常见的初始化技术。先看个双重检查锁定和延迟初始化的例子。

public class Singleton {
    
    private Singleton(){}

    private static Singleton singleton = null;  

    public static Singleton getSafe2Instance() {
        if(singleton == null) {                   //①第一次检查       
            synchronized (Singleton.class) {      //②加锁
                if(singleton == null) {           //③第二次检查
                    singleton = new Singleton();  //④问题出现的地方
                }
            }
        }
        return singleton;
    }
}

   上边的代码有个问题,当线程执行到①处,发现singleton不为空,但是singleton引用的对象有可能还没有完成初始化。那线程获取到的singleton的引用就有可能是空的,

   导致程序出错。为什么会出现线程获取到的singleton的引用时空的呢?我们看一下问题的根源。线程执行到④处,创建了一个对象,这一行代码可以分解为如下三行伪代码。

       memory = allocate();      //1:分配对象内存空间

       ctorInstance(memory);   //2:初始化对象

       singleton = memroy;      //3:设置singleton指向刚分配的内存地址

       第2行和第3行伪代码在编译器里可能会重排序。重排序后的伪代码为:

       memory = allocate();      //1:分配对象内存空间

       singleton = memroy;      //3:设置singleton指向刚分配的内存地址

       ctorInstance(memory);  //2:初始化对象

       如果发生重排序,另一个并发执行的线程B就有可能在①处判断instance不为null,线程B接下来将访问instance锁引用的对象,但此时这个对象可能还没有被线程A初始化。

      那怎么解决这个问题呢?

      1)不允许2和3重排序

      2)允许2和3重排序,但不允许其他线程“看到”这个重排序。

      针对第一条我们可以用volatile来解决,因为volatile有防止重排序的能力。

public class Singleton {
    
    private Singleton(){}

    private volatile static Singleton singleton = null;  

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

  针对第二条,我们可以记录类初始化的解决方案。因为JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,

JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性可以实现另一种线程安全的延迟初始化方案(这个方案被称之为Initialization On Demand Holder idion

public class Singleton {
    
    private Singleton(){}

    private volatile static Singleton singleton = null;  

    public static final Singleton getSafe3Instance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

}

参考:

[1]《Java并发编程艺术》,方腾飞

{2}《Java高并发程序设计》,葛一鸣

原文地址:https://www.cnblogs.com/happyflyingpig/p/9551603.html