单例模式的双重加锁机制为啥要两次检查,第一次检查完不是已经知道了吗?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的双重加锁机制为啥要两次检查,第一次检查完不是已经知道了吗?相关的知识,希望对你有一定的参考价值。

第一张图是单例模式的双重加锁机制。然而我在网上看到有一个解释如下图第二张所示,意思大概是假如把第一个判断去掉会发生什么,他的回答我本来就没搞懂了,然而我又有个新的疑问,如果只是把第二个判断去掉呢,既然第一遍已经检查出了是否为空,那为啥要在lock里检查第二遍呢,感觉有点多余。这个问题困扰了我好久,请各位大神们在看清楚我的疑问再为我解答,万分感谢!

第一张图:针对的是多线程场景
如果两个线程,同时进行为空判断,都为true。此时如果没有lock+第二个判断,必然会创建两个实例。
第二张图:多线程场景可以搞定,但是性能太差
所有针对单例的访问,都需要被锁住,然后一个一个执行。性能太差了。追问

第二张图解释了去掉第一个判断的状况,而我想问的是,既然有第一个判断了,为啥锁里面还要加上第二个判断

追答

线程执行,有快有慢。这里比如有两个线程A,B。
两个线程都同时经过了第一个判断。
此时,操作系统对线程进行调度,使得A线程挂起了。B线程正常执行。
过了一段时间,B线程执行完了,单例对象创建完毕了。A线程此时被唤醒了,可以执行了。
这时候,如果不进行2次判断(B线程早就创建了单例对象),必然会创建两个对象。所以要进行2次判断,防止闯将多个对象

参考技术A 建议你自己模拟多线程,对比下究竟线程安全性、性能有没有区别追问

我的java水平要是会模拟的话,应该不会在这里问这个问题了

我的java水平要是会模拟的话,应该不会在这里问这个问题了

追答

那你的水平也很可能遇不到需要两个锁的情况

追问

但是有些东西不是你遇不到就不去学的呀,有时候面试官会问原理,这个东西我百度不到才迫不得已在这里问的

设计模式。双重检查单例(优化到极致完美),解决单例懒汉式的线程不安全

上文讲到

单纯的加锁就可以

package com.yzdzy.design.singleton;

/**
 * 加锁

 */
public class Mgr04 
    private static Mgr04 mInstance;


    private Mgr04() 
    

    public static synchronized Mgr04 getInstance() 

        if (mInstance == null) 
            // 两个线程里面 会创建多个实例。
            try 
                Thread.sleep(1);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            mInstance = new Mgr04();
        
        return mInstance;
    
    public void m() 
        System.out.println("m");
    
    public static void main(String[] args) 

        for (int i = 0; i < 10; i++) 
            new Thread(() -> 
                System.out.println(Mgr04.getInstance().hashCode());
            ).start();
        
    


but 效率低了对吗

那么有人尝试通过锁同步代码的方式去解决

package com.yzdzy.design.singleton;


public class Mgr05 
    private static Mgr05 mInstance;


    private Mgr05() 
    

    public static Mgr05 getInstance() 

        if (mInstance == null) 
            // 妄图通过减小同步代码库的方式提高效率,然后不可行
            synchronized (Mgr05.class) 
                try 
                    Thread.sleep(1);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                mInstance = new Mgr05();
            
        

        return mInstance;

    

    public void m() 
        System.out.println("m");
    

    public static void main(String[] args) 

        for (int i = 0; i < 10; i++) 
            new Thread(() -> 
                System.out.println(Mgr05.getInstance().hashCode());
            ).start();
        
    


解决了吗

没有。

> Task :Mgr05.main()
1288032892
1696172314
1098833185
471895473
334817404
541982512
662136803
1722732360
1133601195
1313614725

因为他还是持有了懒加载的间隔  和最初的懒加载拥有相同的诟病

解决方案应势而生

双重检查

package com.yzdzy.design.singleton;

/**
 * 懒汉式
 * lazy loading
 * 优点:按需初始化,什么时候用什么时候才初始化
 * 缺点:线程不安全
 */
public class Mgr06 
    //jit volatile 解决指令重排
    private static volatile Mgr06 mInstance;


    private Mgr06() 
    

    public static Mgr06 getInstance() 

        if (mInstance == null) 
            // 妄图通过减小同步代码库的方式提高效率,然后不可行
            synchronized (Mgr06.class) 
                if (mInstance == null) 
                    try 
                        Thread.sleep(1);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    mInstance = new Mgr06();
                
            
        

        return mInstance;

    

    public void m() 
        System.out.println("m");
    

    public static void main(String[] args) 

        for (int i = 0; i < 10; i++) 
            new Thread(() -> 
                System.out.println(Mgr06.getInstance().hashCode());
            ).start();
        
    


> Task :Mgr06.main()
1288032892
1288032892
1288032892
1288032892
1288032892
1288032892
1288032892
1288032892
1288032892
1288032892

以上是关于单例模式的双重加锁机制为啥要两次检查,第一次检查完不是已经知道了吗?的主要内容,如果未能解决你的问题,请参考以下文章

java单例模式(双重检查加锁)的原因

单例模式-双重检查加锁

如何实现一个单例模式 c#双重锁检查

设计模式。双重检查单例(优化到极致完美),解决单例懒汉式的线程不安全

双重检查锁定和单例模式

双重检查锁单例模式为什么要用volatile关键字?