当前位置:网站首页>硬核分析懒汉式单例

硬核分析懒汉式单例

2022-06-11 15:18:00 Evader1997

  本文主要围绕懒汉式单例模式展开,主要是关于线程安全。

  看过我单例模式的下伙伴都知道,实际单例模式的定义很简单,就是有个私有的无参构造,以及对外提供一个获取单例的方法。懒汉式最简单的方式是这样的:

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,那么就节省同步块中代码执行的消耗了。

  到这里代码中还存在问题,这个问题看图会比较清晰:
在这里插入图片描述
  出现这个问题的原因在于线程一还没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是如何执行的。
在这里插入图片描述
  这个流程大家应该都清楚,现在假设A,B两个线程同时来获取单例,A线程进入同步代码块,执行完上述操作后,直接释放锁。这个时候其他线程就会进入同步代码块,此时如果线程A只是刚在内存中开辟了空间,而没有实例化这个空壳子,那么第二个线程走到if时,就不会走if里面的代码。从而返回的是个null。文字有点不好理解,大家结合图片来看。
在这里插入图片描述
最终版代码如下:

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;
    }
}

课外知识,有兴趣的可以研究一下指令重排序,本文不深入探讨,提供一张图供大家理解:
在这里插入图片描述

原网站

版权声明
本文为[Evader1997]所创,转载请带上原文链接,感谢
https://blog.csdn.net/qq_42396796/article/details/118068976

随机推荐