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

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

一、前言

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

二、双重检查模式

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

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步呢?大家请看我画的示意图:

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

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对象。

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值