前言
在面试的时候很多面试官都会让手写单例模式,这个倒是不难,只要理解了代码就能写的下来(就算死记硬背也能默写一遍🤭)~
如果面试官问你,单例模式中为什么要进行二次判空,可能会有一部分小伙伴回答不上来这个问题… 其实我第一次被问到这个问题的时候也不知道原因,后来一通百度找到了答案,本着“独乐乐不如众乐乐的”原则,和大家分享一下我的心得~
单例模式二次判空
首先先贴上单例模式的代码
/**
* 单例模式为什么要二次判空
* @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(); 创建对象时,底层会分为四个指令执行:
- 如果类没有被加载过,则进行类的加载
- 在堆中开辟内存地址,用于存放创建的对象
- 执行构造方法实例化对象
- 将堆中开辟的内存地址赋值给被volatile关键字修饰的instance变量
如果instance变量不使用volatile关键字修饰的话,则可能由于编译器和处理器对指令进行了重排序,导致第4步在第3步之前执行,此时instance引用变量不为null了,但是instance所指向的堆中内存地址中的对象还没被实例化,实例对象还是空的;那么这时候在第一次判空时instance就不为null了,如果这时候去使用instance时就会报空指针异常了。
小结
本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇
希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。
你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西