C05|C05 单例模式 懒汉式的线程安全问题及解决方案(二) 双重检查 & volatile

双重检查基础版

  • 进入getInstance()做第一次检查;
  • 进入synchronized代码块做第二次检查;
  • 这样使用synchronized将大幅降低把synchronized加在方法上的性能开销;
public class LazyDoubleCheckSingleton {private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton() {}public static LazyDoubleCheckSingleton getInstance() { if (lazyDoubleCheckSingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); // 1.分配内存给这个对象 // 2.初始化对象 // 3.设置lazyDoubleCheckSingleton指向刚分配的内存 } } } return lazyDoubleCheckSingleton; }}

双重检查基础版中由重排序引发的问题
  • 做第一次检查的时候,如果lazyDoubleCheckSingleton != null,并不代表lazyDoubleCheckSingleton一定已经初始化完成了,造成这种情形的原因是指令重排序;
  • lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 这句在底层经历了3个动作:
    1.分配内存给这个对象;
    2.初始化对象;
    3.设置 lazyDoubleCheckSingleton 指向刚分配的内存;
    这3个动作中,2和3的动作可能颠倒,其造成的结果就是:Thread-0第一次检查的时候,由于Thread-1先执行3,lazyDoubleCheckSingleton 指向刚分配的内存,导致Thread-0看到的 lazyDoubleCheckSingleton 不为空,直接返回 lazyDoubleCheckSingleton,但此时lazyDoubleCheckSingleton 在Thread-1中还没有初始化,所以造成程序出问题;
  • Java规范中有个 intra-thread semantics 的规定,它保证重排序不会改变单线程的执行结果,重排序可以提高程序的执行性能;
重排序问题的解决方案
  • 不允许重排序;
  • 允许重排序,但不允许另一个线程看到这个重排序;
不允许重排序的解决方案
  • 用 volatile 修饰 lazyDoubleCheckSingleton,就禁止了重排序;
  • 在多线程的时候,多CPU会共享内存,加了 volatile 后,所有的线程都能看到共享内存的最新状态,保证了内存的可见性;
  • 用 volatile 修饰的共享变量在进行写操作的时候,会将当前CPU缓存行的数据写进内存,使得其他CPU缓存了该内存地址的数据无效,从而迫使其他CPU重新从共享内存中获取数据,这样就保证了内存的可见性;
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    推荐阅读