zoukankan      html  css  js  c++  java
  • 双重检查锁单例模式为什么要用volatile关键字?

    DCL, 即(Double Check Lock),中文:双重检查锁定

    1.问题分析

    我们先看看单例模式里面的懒汉式

    public class Singleton{
        private static Singleton singleton;
        
        private Singleton(){}
        
        public static Singleton getInstance(){
            if (singleton ==null){
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    

    以上是无法保证他是线程的安全性,优化如下:

    public class Singleton{
        private static Singleton singleton;
        
        private Singleton(){}
        
        public static synchronize Singleton getInstance(){
            if (singleton ==null){
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    

    以上优化就确保了线程的安全性,但是性能非常的低效,导致性能下降,那么我们用DCL双重检查来优化

    public class Singleton{
        private static Singleton singleton;
        
        private Singleton(){}
        
        public static  Singleton getInstance(){
            if (singleton ==null){   // w1阶段
                synchronize(Singleton.class){
                    if (singleton == null){
                    singleton  = new Singleton();// w2阶段
                    }
                }
            }
            return singleton;
        }
    }
    

    以上代码看起来很完美吧,那我们来分析一下:

    1. 如果检查第一个singleton不为null,则不需要执行以下代码,提高了性能
    2. 如果第一个singleton为neull,即使有多个线程同一个时间判断,但是由于synchronize的存在,只会有一个线程能够创建对象
    3. 当第一个获取锁的线程创建完成后singleton对象后,其他的第二次判断singleton一定不为null,则直接返回已经创建好的singleton对象

    是不是很完美的代码,逻辑上是完全没有问题的. 但是事实上并不是,上面的逻辑在jvm指令排序是存在问题的.jvm创建实例化对象的三个步骤,new 实例背后的指令这个被忽略的问题在于singleton = new Singleton();这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。

    从字节码可以看到创建一个对象实例,可以分为三步:
    1.分配对象内存
    2.调用构造器方法,执行初始化
    3.将对象引用赋值给变量。
    虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
    Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics, 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
    虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。

    上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例(w2阶段),这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入(w1阶段),由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

    由以上分析知道,由于重排序的原因,步骤2,3可能会发生重排序,其过程如下

    memory = allocate();   //1: 分配内存空间
    instance = memory;     //3: 将内存空间的地址赋值给对象的引用
    ctorInstance(memory);  //2: 初始化对象
    

    以上的排序就会导致第二个判断会出错 singleton !=null 但是这时该对象只是个内存地址,并没有初始化实例,所以return的是singleton的内存地址,导致第二个线程获取到的是singleton的内存地址,引用起来就会抛错

    此时就知道错误在哪里了

    singleton = new Singleton();
    

    知道问题根源,解决方法有两个:

    • 不允许初始化阶段步骤发生重排序
    • 允许重排序,但是不允许其他线程看到这个排序(即单例的饿汉单例模式)

    方案一:

    基于以上代码修改:将变量singleton声明为volatile

    public class Singleton {
    
        // 通过volatile关键字来确保安全
        private volatile static Singleton singleton;
        private Singleton(){}
    
        public static Singleton getInstance(){
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    当singleton声明为volatile后,new实例化的步骤就不会重新排序了

    方案二:(使用单例模式的饿汉模式)

    原理: 利用ClassLoder的机制,保证初始化instance时只有一个线程,jvm在类初始化阶段会获取一个锁,这个锁可以同步多个线程对一个类的初始化

    public class Singleton {
    
        private static class SingletonHolder{
            public static Singleton singleton = new Singleton();
        }
    
        public static Singleton getInstance(){
            return SingletonHolder.singleton;
        }
    }
    艾欧尼亚,昂扬不灭,为了更美好的明天而战(#^.^#)
  • 相关阅读:
    【more effective c++读书笔记】【第6章】杂项讨论
    【more effective c++读书笔记】【第5章】技术(7)——让函数根据一个以上的对象类型来决定如何虚化(2)
    【more effective c++读书笔记】【第5章】技术(7)——让函数根据一个以上的对象类型来决定如何虚化(1)
    【more effective c++读书笔记】【第5章】技术(6)——Proxy classes(代理类)
    【more effective c++读书笔记】【第5章】技术(5)——Reference counting(引用计数)(2)
    【more effective c++读书笔记】【第5章】技术(5)——Reference counting(引用计数)(1)
    【more effective c++读书笔记】【第5章】技术(4)——Smart Pointers(智能指针)
    【more effective c++读书笔记】【第5章】技术(3)——要求(或禁止)对象产生于heap之中
    【more effective c++读书笔记】【第5章】技术(2)——限制某个class所能产生的对象数量
    【more effective c++读书笔记】【第5章】技术(1)——将constructor和non-member functions虚化
  • 原文地址:https://www.cnblogs.com/lovelywcc/p/14099695.html
Copyright © 2011-2022 走看看