你是否了解单例模式在多线程下的DCL同步处理机制

多线程下的单例模式DCL安全机制

写在前面:欢迎来到「发奋的小张」的博客。我是小张,一名普通的在校大学生。在学习之余,用博客来记录我学习过程中的点点滴滴,也希望我的博客能够更给同样热爱学习热爱技术的你们带来收获!希望大家多多关照,我们一起成长一起进步。也希望大家多多支持我鸭,喜欢我就给我一个关注吧!

最近在看阳哥的面试题,在多线程那里深有感触!

相信大家在学习单例模式的时候都写过下面这样的单例模式:

public class SingletonDemo {

    private volatile static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()");
    }

    /**
     * @return 单例
     */
    public  static SingletonDemo getInstance(){//此处的方法可能同一时间被多个线程访问,因此会产生多个对象
        if (instance==null){//线程1执行到此处。。。
            instance = new SingletonDemo();//线程2执行到此处。。。
        }
        return instance;//线程3执行到此处。。。
    }

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

此时运行会发现,单例模式出现了多个对象!

这个单例模式在单线程模式下没有任何问题,但是在多线程环境中就会出现多个instance对象

在Java多线程中,我们有时候需要采用延迟初始化来降低初始化类和创建对象的开销DCL(双端检锁机制)是常见的延迟初始化技术。

此处可能会有人会说,给方法加上锁就可以解决了,但是加锁会导致性能大幅降低,因此是不划算的!

因此,我们可以把上面的单例模式改造成如下形式:

public class SingletonDemo {

    private static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()");
    }

    /**
     * DCL双端检锁机制
     * @return 单例
     */
    public  static SingletonDemo getInstance(){
        if (instance==null){//第一次检索,无法防止多线程
           synchronized (SingletonDemo.class){//加锁,保证安全性
               if (instance==null){//第二次检索
                   instance = new SingletonDemo();//此处有一个雷
               }
           }
        }
        return instance;
    }

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

DCL 优势:
上面的代码的好处在这里,如果第一次检查 instance 不为 null ,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized 带来的性能开销。
保证安全性:多个线程试图在同一时间创建对象时,会通过加锁来保证有一个线程创建对象。
保证性能:在对象创建好之后,执行 getInstance() 方法不需要获取锁,直接返回已创建好的对象。

此时测试不会出现多个对象的情况!但是,我们也仅仅是测试了一部分数据,如果用海量的数据测试,会不会出现对个对象呢?

此处的雷就在这个 instance = new SingletonDemo() 这里

相信接触过volatile的小伙伴都知道,它有三大作用:

  1. 可见性
  2. 不保证原子性
  3. 禁止指令重排

这里,问题的关键就在于指令重排序的问题!
我们都知道,java虚拟机为了提高性能,在底层有一个指令重排序的过程!
因此,这里的DCL并不完美,仍然不能保证多线程下的安全性!
在代码进行第一次检索时,代码读取到 instance 不为 null 时,instance 引用的对象有可能还没有完成初始化。因此还是有可能产生安全问题!

解决DCL的指令重排序的安全问题

我们要解决这个问题,需要对instance对象加一个volatile来禁止它指令重排序!

代码优化如下:

public class SingletonDemo {

    private volatile static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()");
    }

    /**
     * DCL双端检锁机制
     * @return 单例
     */
    public  static SingletonDemo getInstance(){
        if (instance==null){
           synchronized (SingletonDemo.class){
               if (instance==null){
                   instance = new SingletonDemo();
               }
           }
        }
        return instance;
    }

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

此时的单例模式在多线程下的DCL双重检测机制才算是线程安全的!

博主后记:

此处的指令重排序博主在这里不详细介绍了,因为涉及到jvm的底层知识点,以及汇编语言和操作系统的知识,博主心知肚明道不清/(ㄒoㄒ)/~~!感兴趣的小伙伴可以自行上网查询相关资料!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章