写点什么

浅谈你对单例类中使用 volatile 关键字的理解 | 超级详细,建议收藏

作者:bug菌
  • 2023-04-16
    上海
  • 本文字数:2724 字

    阅读完需:约 9 分钟

浅谈你对单例类中使用volatile关键字的理解 | 超级详细,建议收藏

👨‍🎓作者:bug 菌

✏️博客:CSDN、掘金、infoQ、51CTO 等

🎉简介:CSDN 博客专家,C 站历届博客之星 Top50,掘金/InfoQ/51CTO 等社区优质创作者,全网粉丝合计 10w+,对一切技术感兴趣,重心偏 Java 方向;硬核公众号「 猿圈奇妙屋」,欢迎小伙伴们的加入,一起秃头,一起变强。

..

✍️温馨提醒:本文字数:3999 字, 阅读完需:约 9 分钟

        如果小伙伴们在批阅文章的过程中觉得文章对自己有帮助,请别吝啬手中的赞呀,大胆的把文章点亮👍,相信你点赞了好的文章,平台也会经常给你推荐高质量好文,您的点赞三连(收藏+关注+留言)就是对 bug 菌写文道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️

一、前言🔥

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

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

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,以上就是我这期的全部内容啦,如果还想学习更多,可以看看我的往期热文推荐哦,不积跬步,无以至千里; 不积小流,无以成江海,一口吃不成一个大胖子,加油!咱们下期拜拜~~

文末🔥

        安利一个超牛超硬核的专栏《springboot零基础入门教学》,此专栏包含数个完整项目从零到一的搭建,以及对 SpringBoot 入门程序原理剖析,在会用的基础上剖析源码加深理解并拓展知识点.希望能帮助到更多小伙伴们。

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


感谢认真读完我博客的铁子萌,在这里呢送给大家一句话,不管你是在职还是在读,绝对终身受用。

时刻警醒自己:

抱怨没有用,一切靠自己;

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


发布于: 刚刚阅读数: 5
用户头像

bug菌

关注

公众号 | 猿圈奇妙屋 2020-07-30 加入

CSDN博客专家,历届博客之星Top30,掘金年度人气作者No.40,掘金/InfoQ/51CTO等社区优质创作者,全网粉丝合计10w+,硬核公众号「猿圈奇妙屋」,欢迎小伙伴们的加入,一起学习,一起变强。

评论

发布
暂无评论
浅谈你对单例类中使用volatile关键字的理解 | 超级详细,建议收藏_volatile_bug菌_InfoQ写作社区