单例模式

/ 设计模式

单例(Singleton)

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

GoF23种设计模式中,属于创建型模式( Creational patterns)

现实生活中很多东西都是唯一的。比如:地球,太阳等。

对应代码中,如果我们要使用地球这个对象,获取地球的水,石油等资源时,地球这个对象就应该设计成单例模式。

名词解释

在进行实际编码之前,我们应该知道几个名词的含义。

延迟加载:仅在使用到数据时,才进行数据的加载。这样可以加快程序的加载速度,同时可以节省一些不必要的资源占用。

线程安全:当多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为。

一图以蔽之

singleton-pattern

coding 演进

懒汉式

延迟加载:是

线程安全:否

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    public static LazySingleton getInstance(){

        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

    /**
     * 私有化构造器
     */
    private LazySingleton() {
    }

}

懒汉式最大的缺点就是线程不安全。

假设有两个线程A、B。A线程在执行lazySingleton = new LazySingleton();时,进入阻塞状态,此时lazySingleton仍为null,因此,B线程仍能进入执行lazySingleton = new LazySingleton();

此时,A、B线程得到的对象就不是同一个了,这显然违反了单例的基本定义。

为了解决线程安全问题,则需加同步的关键字。

懒汉式-线程安全

延迟加载:是

线程安全:是

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    public static synchronized LazySingleton getInstance() {

        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

    /**
     * 私有化构造器
     */
    private LazySingleton() {
    }

}

通过这种同步锁机制虽然解决了线程安全问题,但synchronized本身就需要一定的资源开销,另外synchronized锁的是静态方法,其就是锁的LazySingleton.class,锁的范围过大,性能不佳。

为了提供性能,于是有了Double-Check式。

双重检查

延迟加载:是

线程安全:是

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton doubleCheckSingleton = null;

    private DoubleCheckSingleton() {

    }

    public static DoubleCheckSingleton getInstance() {
        if (doubleCheckSingleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (doubleCheckSingleton == null) {
                    //下面new对象时有重排序问题
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }

}

双重检查,这里把锁的范围从类锁,降为方法锁,提升了系统性能。同时也兼具延迟加载和线程安全。

但是,这样写会有指令重排序问题

new DoubleCheckSingleton()时会进行几个操作:

1、在堆区分配对象需要的内存

2、初始化对象

3、将堆区对象的地址赋值给doubleCheckSingleton

这里的2、3操作可能重排序,按1、3、2顺序执行。

此时,doubleCheckSingleton已经不为null,但对象并未创建,从而引起错误。

为了避免重排序的发生可以采用以下两种方式:

1、禁止重排序

2、屏蔽重排序

禁止重排序-双重检查

延迟加载:是

线程安全:是

public class DoubleCheckSingleton {

    private static volatile DoubleCheckSingleton doubleCheckSingleton = null;

    private DoubleCheckSingleton() {

    }

    public static DoubleCheckSingleton getInstance() {
        if (doubleCheckSingleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (doubleCheckSingleton == null) {
                    //下面new对象时有重排序问题
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }

}

使用volatile关键字可以禁止重排序问题。至此,Double-Check 已进化至究极体。

屏蔽重排序-静态内部类

延迟加载:是

线程安全:是

public class StaticInnerClassSingleton {

    private static class Instance{
        private static StaticInnerClassSingleton staticInnerClassSingleton =  new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance(){
        return Instance.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {
    }

}

因为内部类和静态内部类都是延时加载的,也就是说只有在明确用到内部类时才加载。只使用外部类时不加载。

故,只有在调用getInstance()时,StaticInnerClassSingleton.class才会被初始化。

且,在执行类的初始化期间,JVM会去获取一个锁,该锁可以同步多个线程对同一个类的初始化。从而屏蔽了静态内部类重排序对外部的影响。

饿汉式

延迟加载:否

线程安全:是

public class HungrySingleton{

    private final static HungrySingleton HUNGRY_SINGLETON;

    static {
        HUNGRY_SINGLETON = new HungrySingleton();
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

    private HungrySingleton() {
    }

}

这种模式虽然简单易实现,但非延迟加载。

枚举类单例

延迟加载:否

线程安全:是

public enum EnumSingleton {
    INSTANCE;

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

这么写我们很难看出枚举类是怎么实现单例的,是因为枚举类用了java的语法糖。

接下来让我们用jad反编译下这个类的class,看下正真的运行代码的样子。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java

package top.ordin.tool.design.pattern.creational.singleton;


public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(top/ordin/tool/design/pattern/creational/singleton/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

通过反编译,我们不难看出单例自己实现了私有构造器,而且还通过静态代码块初始化了类对象。

这种语法糖包装后的枚举类跟饿汉式很像。

但由于这种写法出现较晚,所以日常很少见,但保不定这种方式将会成为主流。

单例序列化

序列化时,对枚举类型做了特殊处理,使得枚举类在序列化与反序列化后仍能保持单例。

其他类型的单例模式就没这待遇了,但是序列化仍保留了一个重写的读方法。其他单例类型通过添加该方法仍能保持单例不被打破。

private Object readResolve(){
    return INSTANCE;
}

单例防止反射

java可以通过反射的方式创建对象。

但是,在反射创建时,会对枚举类做特殊处理,禁止反射创建枚举对象。

测试代码

public static void main(String[] args) throws Exception {
    //反射创建
    Class enumInstanceClass = EnumInstance.class;
    Constructor constructor = enumInstanceClass.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    EnumInstance instance = (EnumInstance) constructor.newInstance("demo", 123);
    
    EnumInstance instance1 = EnumInstance.getInstance();
    
    System.out.println(instance == instance1);
}

output

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.geely.design.pattern.creational.singleton.Test.main(Test.java:53)

饿汉式和静态内部类因为在类初始化时就被加载。因此可以在构造器中添加判断条件,如果对象已被创建就抛出异常,以此来防止反射调用。

private HungrySingleton(){
    if(HUNGRY_SINGLETON != null){
        throw new RuntimeException("Cannot reflectively create this objects");
    }
}
private StaticInnerClassSingleton() {
    if (Instance.staticInnerClassSingleton != null){
        throw new RuntimeException("Cannot reflectively create this objects");
    }
}

懒汉式就放弃吧...

推荐单例写法

综上所述,一般比较推荐静态内部类和枚举类写法。

静态内部类

public class StaticInnerClassSingleton {

    private static class Instance{
        private static StaticInnerClassSingleton staticInnerClassSingleton =  new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance(){
        return Instance.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {
        if (Instance.staticInnerClassSingleton != null){
            throw new RuntimeException("Cannot reflectively create this objects");
        }
    }

    private Object readResolve(){
        return Instance.staticInnerClassSingleton;
    }

}

枚举类

public enum EnumSingleton {
    INSTANCE;

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}