饿汉和懒汉

<aside> 💡 并发的情况下,普通的单例模式是没有效果的,一般要是有DCL模式,加上volatile防止指令重排来实现单例模式。

</aside>

先看下面的代码

public class Singleton {
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if(null == instance) {                    // line A
            instance = new Singleton();        // line B
        }
        
        return instance;
        
    }
}

假设这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然为空,因此线程二执行了new Singleton()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Singleton()操作,这样问题就来了,new出了两个instance,这还能叫单例吗?

紧接着,我们再做单例模式的第二次尝试:

public class Singleton {
    private static Singleton instance = null;
    public synchronized static Singleton getInstance() {
        if(null == instance) {                    
            instance = new Singleton();            
        }
        
        return instance;
        
    }
}

比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上来说很不划算。

继续改

public class Singleton {
    private static Singleton instance = null;
    public  static Singleton getInstance() {
        synchronized (Singleton.class) {
            if(null == instance) {                    
                instance = new Singleton();            
            }
        }
        
        return instance;
        
    }
}

基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建instance实例的时候进行同步,因此有了下面的写法——双重锁定检查(DCL,Double Check Lock)。

DCL模式

<aside> 💡 注意指令重排问题、加上volatile

</aside>

public class Singleton {
    private static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {    // 线程二检测到instance不为空
            synchronized (Singleton.class) {
                if(null == instance) {                    
                    instance = new Singleton();    // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)    
                }
            }
        }
        
        return instance;    // 后面线程二执行时将引发:对象尚未初始化错误
        
    }
}

看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗?

如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

1)给instance实例分配内存;

2)初始化instance的构造器;

3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)