我说我会懒汉式单例,面试官连续问了4个问题

本文主要围绕懒汉式单例模式展开,这是我面试时经常被追问的问题,深入剖析懒汉式单例。

看过我单例模式的小伙伴都知道,实际单例模式满足两个条件即可,一是有个私有的无参构造,二是对外提供一个获取单例的方法。懒汉式最简单的方式是这样的:

public class LazySingeton {
    private static LazySingeton singeton;

    private LazySingeton() {

    }

    public LazySingeton getSingeton() {
        if (singeton == null) {
            singeton = new LazySingeton();
        }
        return singeton;
    }
}

这个代码在单线程情况下是没有问题的,但是在多线程环境下就需要考虑线程安全的问题。举个不恰当的例子,但是印象肯定深刻,话说男生宿舍卫生间就一个坑位,A进卫生间时没有锁门,这个时候B肚子疼直接推门而进,立即脱下裤子蹲下。后面发生的就自行脑补了,总之这肯定是有问题的,A还没用完坑位,B就进来了,这不是强人所男,男上加男吗?迎男而上这种事发生的概率还是比较小的。所以解决这个问题的方法就是A进卫生间后把卫生间门锁上。

巧了,Java中正好有锁,先加上锁,就像这样:

public class LazySingeton {
    private static LazySingeton singeton;

    private LazySingeton() {

    }
	
	// 添加synchronized锁,保证获取单例的方法只被一个地方调用
    public synchronized LazySingeton getSingeton() {
        if (singeton == null) {
            singeton = new LazySingeton();
        }
        return singeton;
    }
}

通过synchronized关键字可以保证线程安全问题,但是当前这种写法是可优化的,具体怎么优化呢?还是上卫生间这个例子,有些卫生间的门是带提示功能的,关门显示有人,开门显示没人。这种门大家伙一看就知道里面是否有人,如果是一个不带提示功能的门,正常操作都是敲敲门,问问有人没。

上述代码就是敲门的操作,这是比较消耗性能的,每一个请求过来都需要判断getSingeton()方法是否加锁,相当于有个哥们正在坑位上蹲着,每来一个哥们敲一下门,嗨,里面有人吗?

所以我们应该加一个提示功能,看看卫生间是否有没有人?对应到代码里就是对单例singeton进行判断,是否为null。如果为null就不再初始化(不问坑位上有没有人了),直接返回(直接走了)。这种方式是不是非常有效率呢,代码如下:

public class LazySingeton {
    private static LazySingeton singeton;

    private LazySingeton() {

    }

    public LazySingeton getSingeton() {
        // 判断单例是否为null,不为null直接返回即可(减少每次判断是否加锁的性能消耗)
        if (singeton == null) {
            synchronized(LazySingeton.class){
                singeton = new LazySingeton();
            }
        }
        return singeton;
    }
}

看完这版代码后脑洞比较大的同学会有这个疑问,这样不是还是需要判断是否加锁?思考的很对,这里**加锁在所难免,为了保证线程安全,敲门是肯定要敲的,但是减少敲门的次数是否等于性能提升了呢?**只有在singeton为null时才会去判断是否加锁,如果singeton不为null,那么就节省同步块中代码执行的消耗了。

到这里代码中还存在问题,这个问题看图会比较清晰:
QQ截图20210620105844.png

出现这个问题的原因在于线程一还没new,线程二就通过了if(singeton = null)的判断,偷偷进入到后面的流程,这就好比一个人上卫生间还没来得及上锁,另一个人冲了进来。解决这个问题很简单,既然你跳过了if(singeton = null)的判断,那么我就在判断一次!,这样线程二即使侥幸逃过第一轮验证,也逃不过第二轮,从而保证只创建一个对象。具体代码如下:

public class LazySingeton {
    private static LazySingeton singeton;

    private LazySingeton() {

    }

    public LazySingeton getSingeton() {
        // 第一次判断
        if (singeton == null) {
            synchronized (LazySingeton.class) {
                // 第二次判断
                if (singeton == null) {
                    singeton = new LazySingeton();
                }
            }
        }
        return singeton;
    }
}

以上内容还有没有问题?答案是有的,接下来涉及到JVM相关知识:指令重排序。接下来的内容初学者大改能理解就行,第一次看的不必深挖。首先大家先看下图,看看singeton = new LazySingeton();这行代码在JVM是如何执行的。
QQ截图20210620115815.png
这个流程大家应该都清楚,现在假设A,B两个线程同时来获取单例,A线程进入同步代码块,执行完上述操作后,直接释放锁。这个时候其他线程就会进入同步代码块,此时如果线程A只是刚在内存中开辟了空间,而没有实例化这个空壳子,那么第二个线程走到if时,就不会走if里面的代码。从而返回的是个null。文字有点不好理解,大家结合图片来看。
QQ截图20210620121228.png

最终版代码如下:

public class LazySingeton {
    // 增加volatile修饰符,防止指令重排序
    private volatile static LazySingeton singeton;

    private LazySingeton() {

    }

    public LazySingeton getSingeton() {
        if (singeton == null) {
            synchronized (LazySingeton.class) {
                if (singeton == null) {
                    singeton = new LazySingeton();
                }
            }
        }
        return singeton;
    }
}

课外知识,有兴趣的可以研究一下指令重排序,本文不深入探讨,提供一张图供大家理解:
微信图片_20210620121838.jpg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jayden 

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

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

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

打赏作者

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

抵扣说明:

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

余额充值