单例模式确保一个类只有一个实例,并提供一个全局访问点。

在很多实际场景中,我们希望某个类只能有一个实例化的对象,例如数据连接池,日志对象等等,这个时候我们就要使用单例模式了,单例模式的核心思想是私有化构造器,防止其他类任意实例化该类,具体的实现有多种,每种都会有其优缺点,让我们来仔细看看各种实现的差异吧。

依据我写代码的习惯,先上UML类图,我用的工具是StarUML)

第一种实现:

Java DCL 单例模式真的需要对变量加Volatile_java



public class MyClass {
    
    private static MyClass instance;

    private MyClass() {}
    
    public static MyClass getInstance() {
        if (instance == null) {
            instance = new MyClass();
        }
        return instance;
    }

}

优点:可以实现“延迟实例化”。

延迟实例化?看起来好像很叼的样子,让我想起了延迟加载,其实实现起来很简单,也就是说如果MyClass.getInstance()这个方法不被调用的话,JVM里面永远都不会有MyClass的实例,如果getInstance方法被调用的话,程序会首先判断静态变量instance是否为null,如果为null则第一次(也是最后一次)使用new关键字调用私有构造方法实例化一个MyClass对象并赋值给静态变量instance,这样instance变量就会持有MyClass的对象,当getInstance方法第二次被调用的时候,程序直接返回一个已经存在在JVM内存静态区的instance.

仔细想想,MyClass对象真的只会创建一次吗?有没有那种场景,万一的巧合,MyClass类创建了两个实例呢?答案是肯定的,在多线程场景中,MyClass对象很有可能被实例化多次,这样就引出了第一种实现方式的局限性。

缺点:无法保证在多线程场景中依然可以保持单例。 

考虑到第一种实现方式的利弊,我们演化出了第二种实现方式

public class MyClass {
    
    private static MyClass instance;

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

}

从以上代码可以看到,我们在getInstance方法上使用了synchronized关键字,这样我们这个单例模式可以经得住多线程场景的考验了吧?确实,synchronized方法不会再惧怕两个线程同时进入getInstance方法创建两个实例的尴尬情况,但是synchronized关键字却引出了另外一个问题,性能。当然如果MyClass方法半天都难得被调用一次,你完全可以采用这种方式实现你的单例模式,但是想想,有没有那么一种可能,MyClass.getInstance方法要被很多个类同时调用很多次,这个时候你就不得不考虑性能问题了,综上所述,第二种实现方式的优缺点显而易见:

优点:既能实现延迟实例化也能保证在多线程环境下只有一个实例

缺点:性能问题 

有没有一种办法,让我们能够保留第二种方式的优点同时解决它的缺点呢?办法当然总是比困难多,不然人类社会文明怎样才能保持进步呢:)

第三种方式:

public class MyClass {
    
    private static MyClass instance = new MyClass();

    private MyClass() {}
    
    public static MyClass getInstance() {
        return instance;
    }

}

第三种单例实现方式也就是我们通常所说的饿汉单例模式,这种方式将MyClass的对象赋值给类变量,这样JVM在加载MyClass类的时候就会把MyClass的对象实例化出来并放置到JVM内存静态区,下次有其他类调用MyClass.getInstance方法时直接返回静态区的实例,避免了再次创建对象。但是这种饿汉单例模式也有一个明显的缺点,那就是无法实现延迟实例化,对象的实例化放到了类加载阶段而不是getInstane方法首次调用的时候,初看起来这也没什么不好,但是仔细想想:如果我们有很多个类都是实现的饿汉单例模式,而每一个类实例化的时候都需要使用很多资源例如获得数据连接或建立数据池等等,那JVM在加载这些类的时候是不是需要花很长时间甚至出现OutOfMemory异常呢,而且依据OO设计依赖倒置原则:变量不可以持有具体类的引用。综上所述,貌似第三种实现方式也需要改良。

优点:能够解决多线程环境下的单例问题也不会出现性能问题

缺点:无法实现延迟实例化 

根据以上3种方式的优缺点,第四种终极解决方案来了:

public class MyClass {
    
    private volatile static MyClass instance = null;

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

}

第四种方式使用了双重检查加锁机制,这种方式保证了只有第一次创建对象的时候才会同步,从而既保证了多线程场景下只有一个实例被创建也避免了同步方法带来的性能问题,但是是不是这种解决方案没有任何缺点呢? 答案是否定的,Java 1.5开始才有volatile关键字,如果你使用的是1.4及之前的版本,还是忘了这种实现方式吧!

优点:可以实现延迟实例化,可以保证多线程场景下单例的问题

缺点:Java 1.5 之前的版本不适用

综上分析,各位在采用单例模式的时候可以结合项目的实际情况采用,设计模式并不是死板硬套的,关键在于解决项目中出现的实际问题。