likes
comments
collection
share

面试官考我,单例类中为什么要使用volatile关键字?我表示...😏

作者站长头像
站长
· 阅读数 19
环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

一、前言

    有一期《【java笔试题】如何手写一个单例类?》一文中我们提到,由于volatile关键字它可以禁止指令重排序来保证一定的有序性,故而解决了多线程情况下双重检查模式单例空指针问题。

    那你们知道为何多线程下双重检查模式会导致空指针异常吗?一开始我也没多想,但是这样的学习模式是不对的,道听途说不如自己亲手试验,检验出真知,于是我花了半个小时,终于搞清楚了!

二、双重检查模式

    给大家再回顾一下懒汉单例之双重检查模式是如何手撕的,代码仅供参考。大家请看:

public class DoubleLazySingLeton {

    private static DoubleLazySingLeton instance;

    // 私有构造方法
    private DoubleLazySingLeton() {
        System.out.println("生成DoubleLazySingLeton实例一次!");
    }

    // 对外提供静态方法获取该对象
    public static DoubleLazySingLeton getInstance() {
        // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例.
        if (instance == null) {
            //instance未实例化的时候才加锁
            synchronized (DoubleLazySingLeton.class) {
                // 抢到锁之后再次判断是否为null
                if (instance == null) {
                    instance = new DoubleLazySingLeton();
                }
            }
        }
        return instance;
    }
}

    看完如上单例代码实现,大佬都陷入了深思,比如我(大佬骂骂咧咧的说道:谁写的?打发要饭呢!写的啥玩意,重写)。

    为什么反响会如此剧烈,有同学可能会说写的不是挺合理的嘛? 不着急,接着往下看,我会告诉你为什么。

三、案例分析

    我们在学习volatile关键字的时候,发现它可以禁止指令重排序从而保证执行有序性,等价于使用它就可以保证new DoubleLazySingLeton()创建对象实例化过程时的顺序不变。

    具体volatile是如何保证的呢?这就得从volatile关键字的源码下手了,这节我们先不深究,重点是要理解new DoubleLazySingLeton()为何会出现不按顺序实例化的问题?而且为何要保证实例化的顺序性呢?这才是我们本文的重中之重,带着这两个问题我们接着往下看。

    首先,我们都知道创建一个对象可以分为5步,对吧。

     那5步呢?大家请看我画的示意图:

面试官考我,单例类中为什么要使用volatile关键字?我表示...😏

    所以,你们可以思考一件事了,如下实例化有何问题?

instance = new DoubleLazySingLeton();

    从表面上看,没有任何问题,但是结合双重检测模式来看,那就非常有问题了。

    虽然单线程下的双重检测模式非常完美,但是在并发环境中就是bug般的存在。因为new DoubleLazySingLeton()实例化它并不是一个原子操作,我们看创建对象过程也可以得知。因此我们可以把这个实例化抽象成三条jvm指令:如下:

memory = allocate(); //1:分配对象的内存空间 
initInstance(memory); //2:初始化对象 
instance = memory; //3:设置instance指向刚分配的内存地址

    上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以jvm可以以“优化”为目的对它们进行重排序,经过重排序后顺序假设如下:

memory = allocate(); //1:分配对象的内存空间 
instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象

    可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段内存还没被初始化。 所以,你们发现什么问题了么?如果还没发现不着急,请接着往下看。

四、场景模拟

    现在我们再来模拟个场景:存在两个线程:线程A与线程B,它两同时调用getSingleton()获取instance对象。

然后按如下时间段进行执行,请问大家,但线程执行完会发什么?

面试官考我,单例类中为什么要使用volatile关键字?我表示...😏

    很明显,线程B会访问到一个还未完成初始化的"半"个instance对象。为什么?当线程A执行到T2时间后已经将instance指向了一块内存空间,此刻线程B调用getInstance(),执行到 if ( instance == null ) 语句时,instance == null结果肯定为false,因为instance已经被指定内存了不为null,然后直接执行return instance 语句结束,结果返回了一个没有完成初始化的“半个”单例。

    也就是我一开始给大家说的指令重排之后,先执行了A3,再执行A2,这样本身没有问题,但如果遇到有其他线程碰巧在你指令重排后没实例化完成前调用getInstance()获取instance对象,这肯定会抛异常(如上示例)。最终的结果肯定是送你NullPointerException大礼包。

五、总结

    所以对于双重检查模式下的单例而言,就会存在线程安全问题,怎么解决该线程安全,那就可以用volatile来保证。

    顾解决线程安全的核心就是要保证instance对象顺序实例化,而volatile可以禁止指令重排序,这样尽管是多线程环境下,也不用担心instance实例化所带来的线程安全问题啦,这么讲,大家可得明白没有。

... ...

    ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看我的往期热文推荐哦,每天积累一个奇淫小知识,日积月累下去,你一定能成为令人敬仰的大佬。

    「赠人玫瑰,手留余香」,咱们下期拜拜~~

文末💭

    我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

面试官考我,单例类中为什么要使用volatile关键字?我表示...😏

    感谢认真读完我博客的铁子萌,在这里呢送给大家一句话,不管你是在职还是在读,绝对终身受用。 时刻警醒自己:抱怨没有用,一切靠自己

想要过更好的生活,那就要逼着自己变的更强,生活加油。