前言

在面试中被问到频率最高的设计模式是单例,因为它写起来很简单,而且了解单例模式的都知道,它有饿汉式、懒汉式、DCL(双重锁判断)、静态内部类以及枚举等多种写法。但说实话,在实际应用中,单例用到的并不是很多。但作为设计模式的基本模式之一,我们也有必要了解单例是否满足需求,例如线程是否安全,是否延迟加载,反射是否安全,序列化是否安全,这是本文重点关注的问题。
单例模式就是在应用的整个生命周期中只存在一个实例。它有很多好处,避免实例对象的重复创建,减少实例对象的重复创建,减少系统开销。例如spring容器中管理的Bean默认就是单例的。

五种单例模式

饿汉式

写法

public class HungrySingleton implements Serializable{

    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return singleton;
    }

}
复制代码

之所以implements Serializable(下同),是为了后面测试序列化是否安全的需要,一般情况不用加。

特性

饿汉式在类加载时期就已经初始化实例,而我们知道类加载是线程安全的,所以饿汉式是线程安全的。很明显,它不是延迟加载的,这也是饿汉式的缺点。通过下面的测试方法1,饿汉式不是反射安全的,因为通过反射构造方法产生了两个实例。通过测试方法2,饿汉式也不是序列化安全的。

测试方法1:

public static void main(String [] args) {
        //测试饿汉式反射是否安全
        reflectTest();
    }
    
    private static void reflectTest() {
        HungrySingleton singleton1 = HungrySingleton.getInstance();
        HungrySingleton singleton2 = null;
        try {
            Class<HungrySingleton> clazz = HungrySingleton.class;
            Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
复制代码

运行结果:

测试方法2:

public static void main(String [] args) {
        //测试饿汉式序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        HungrySingleton singleton1 = HungrySingleton.getInstance();
        HungrySingleton singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (HungrySingleton) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
复制代码

运行结果:

懒汉式

写法

public class LazySingletonThreadNotSafe implements Serializable{

    private static LazySingletonThreadNotSafe singleton = null;

    private LazySingletonThreadNotSafe() {
        
    }
    public static LazySingletonThreadNotSafe getSingleton() {
        if (singleton == null) {
            singleton = new LazySingletonThreadNotSafe();
        }
        return singleton;
    }

}
复制代码

特性

懒汉式在饿汉式的基础上进行了改造,将实例的初始化从类加载过程移到getInstance()方法真正调用时进行。所以具备了延迟加载,但失去了线程安全性。下面的DCL在此基础上增加了线程安全。从测试方法1和2可知,懒汉式反射不安全,序列化也不安全。 测试方法1:

public static void main(String [] args) {
        //测试懒汉式反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
        LazySingletonThreadNotSafe singleton2 = null;
        try {
            Class<LazySingletonThreadNotSafe> clazz = LazySingletonThreadNotSafe.class;
            Constructor<LazySingletonThreadNotSafe> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
复制代码

运行结果:

测试方法2:

public static void main(String [] args) {
        //测试懒汉式序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
        LazySingletonThreadNotSafe singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (LazySingletonThreadNotSafe) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }

复制代码

运行结果:

DCL(双重锁判断Double Check Lock)

写法

public class LazySingletonThreadSafe implements Serializable{

    private volatile static LazySingletonThreadSafe singleton =null;

    private LazySingletonThreadSafe() {
        
    }

    public static LazySingletonThreadSafe getSingleton() {
        if (singleton == null) {   //1
            synchronized (LazySingletonThreadSafe.class) {   //2
                if (singleton == null) {   //3
                    singleton = new LazySingletonThreadSafe();  //4
                }
            }
        }
        return singleton;
    }
}
复制代码

特性

DCL是在懒汉式基础上的改进,跟懒汉式唯一不同的是DCL是线程安全的。你可能会问,有了synchronized保证线程安全,为啥还要加volatile修饰?因为DCL本身存在一个致命缺陷,就是重排序导致的多线程访问可能获得一个未初始化的对象。
我们知道singleton = new LazySingletonThreadSafe();这行代码在JVM看来有这么三步:
1、为对象分配存储空间
2、初始化对象
3、将singleton引用指向第一步中分配的内存地址
第2步和第3步可能存在重排序。假设线程A按2、3步颠倒的顺序执行代码(发生了重排序),先执行了第3步,此时singleton引用已经指向了第一步中分配的内存地址,当线程B执行getSingleton()方法时,发现singleton != null,就执行获得了还没有初始化的singleton,这样就出问题了。我们知道volatile的性质是保证多线程环境下变量的可见性以及禁止指令重排序,所以要加volatile。

静态内部类

写法

public class StaticInnerSingleton implements Serializable{

private StaticInnerSingleton() {

}

/**
 * 静态内部类,它和饿汉式一样,基于类加载机制的线程安全,又做到延迟加载。
 * SingletonHolder是一个内部类,当外部类StaticInnerSingleton被加载的时候不会被加载,
 * 调用getSingleton方法的时候才会被加载。
 */
private static class SingletonHolder {
    private static final StaticInnerSingleton singleton = new StaticInnerSingleton();
}

public static StaticInnerSingleton getSingleton() {
    return SingletonHolder.singleton;
}
复制代码

}

特性

静态内部类和饿汉式一样是线程安全的,同时又做到了延迟加载。但是反射不安全,序列化也不安全。
测试方法1:

public static void main(String [] args) {
        //测试静态内部类反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
        StaticInnerSingleton singleton2 = null;
        try {
            Class<StaticInnerSingleton> clazz = StaticInnerSingleton.class;
            Constructor<StaticInnerSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }

复制代码

运行结果:

测试方法2:

public static void main(String [] args) {
        //测试静态内部类序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
        StaticInnerSingleton singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (StaticInnerSingleton) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
复制代码

运行结果:

枚举

写法(简单)

public enum EnumInstance implements Serializable{
    INSTANCE;
}
复制代码

特性

用java反编译工具看看Enum的源码,跟饿汉式一样,是在类加载时就初始化了,是线程安全的,所以并不是延迟加载的。

public final class EnumSingleton extends Enum {

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

    public static EnumSingleton valueOf(String s) {
        return (EnumSingleton)Enum.valueOf(test/singleton/EnumSingleton, s);
    }

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

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

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

测试方法:

public static void main(String [] args) {
        //测试枚举反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        EnumInstance singleton1 = EnumInstance.INSTANCE;
        EnumInstance singleton2 = null;
        try {
            Class<EnumInstance> clazz = EnumInstance.class;
            Constructor<EnumInstance> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance("test",1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
复制代码

运行结果:

直接不让反射了,说明枚举是反射安全的。在 constructor.newInstance()源码中,有这么几行,是枚举类型直接抛异常了。最后枚举单例也是序列化安全的,可以自己测试一下。

总结

通过以上测试,了解了五种单例模式各有优缺点,没有说哪种单例模式最好,只有满足需求的才是最合适的。