前言
在面试中被问到频率最高的设计模式是单例,因为它写起来很简单,而且了解单例模式的都知道,它有饿汉式、懒汉式、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()源码中,有这么几行,是枚举类型直接抛异常了。最后枚举单例也是序列化安全的,可以自己测试一下。
总结
通过以上测试,了解了五种单例模式各有优缺点,没有说哪种单例模式最好,只有满足需求的才是最合适的。