大聪明教你学Java | 单例模式为什么要进行二次判空

前言

在面试的时候很多面试官都会让手写单例模式,这个倒是不难,只要理解了代码就能写的下来(就算死记硬背也能默写一遍🤭)~

如果面试官问你,单例模式中为什么要进行二次判空,可能会有一部分小伙伴回答不上来这个问题… 其实我第一次被问到这个问题的时候也不知道原因,后来一通百度找到了答案,本着“独乐乐不如众乐乐的”原则,和大家分享一下我的心得~

单例模式二次判空

首先先贴上单例模式的代码

/**
 * 单例模式为什么要二次判空
 * @description: Singleton
 * @author: 庄霸.liziye
 * @create: 2021-11-24 14:47
 **/
public class Singleton {

    private static Singleton instance = null;
    private Singleton(){}

    public static Singleton getInstance(){
        //第一次判空
        if (instance == null) {
            synchronized(Singleton.class){
                //第二次判空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

咱们可以看到代码中有两次 instance == null 的判断,这就是前面所说的二次判空,接下来我们慢慢分析一下这两次判断分别有什么作用。

第一次判空

第一次判空的位置是在synchronized锁之前,我们可以想象一下,如果把第一次判空删掉会是什么情况呢🤔

如果拿掉第一次判空后,在行的时候就会直接运行synchronized,那么每次调用 getInstance() 方法的时候都会得到一个静态内部锁,也就会增加获取锁和释放锁的开销,降低了效率。所以在synchronized锁前面再加一次判断,就大大降低synchronized块的执行次数,也就降低了获取锁和释放锁的开销。

第二次判空

接下来我们再看看第二次判空(●’◡’●)
有些小伙伴会想,要是没有第二次判空应该也可以吧~ 确实可行,不过也只是针对单线程的情况下可行。

那么请各位开动一下聪明的小脑瓜,一起脑补一下:如果在多线程的情况下,不加上二次判空会有个什么现象呢?

假如现在有两个线程,分别是线程A 和 线程B,首先两个线程执行到第一个if ,这时线程A先拿到了锁,线程B就被堵塞了;接下来线程A完成了instance的初始化,创建了内存,然后线程A就释放了锁;回头再看看一直被堵塞的线程B,线程A释放了锁以后,线程B就拿到了锁,继续向下执行…

聪明的小伙伴可能已经想到了,如果此时没有第二次判空 ,那么就还会再执行new操作,这样就可以达到一个防止重复创建的目的,节省了内存资源~

升级版

/**
 * 单例模式为什么要二次判空
 * @description: Singleton
 * @author: 庄霸.liziye
 * @create: 2021-11-24 14:47
 **/
public class Singleton {

    private volatile static Singleton instance = null;
    private Singleton(){}

    public static Singleton getInstance(){
        //第一次判空
        if (instance == null) {
            synchronized(Singleton.class){
                //第二次判空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

升级版的代码改动不大,就是单例instance使用volatile关键字修饰了一下下,这么做是为了禁止指令重排。

有些小伙伴可能不常用volatile关键字,这里也简单的说几句。

volatile是一个特征修饰符(type specifier),volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值(说白了就是告诉编译器不要把这个变量优化掉了,该从哪里取数据就从哪里取数据)。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

接下来咱们再说说重排序的问题~
在 Instance = new SingletonClass(); 创建对象时,底层会分为四个指令执行:

  1. 如果类没有被加载过,则进行类的加载
  2. 在堆中开辟内存地址,用于存放创建的对象
  3. 执行构造方法实例化对象
  4. 将堆中开辟的内存地址赋值给被volatile关键字修饰的instance变量

如果instance变量不使用volatile关键字修饰的话,则可能由于编译器和处理器对指令进行了重排序,导致第4步在第3步之前执行,此时instance引用变量不为null了,但是instance所指向的堆中内存地址中的对象还没被实例化,实例对象还是空的;那么这时候在第一次判空时instance就不为null了,如果这时候去使用instance时就会报空指针异常了。

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不肯过江东丶

您的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值