在面试中被问到频率最高的设计模式是单例,由于它写起来很简单,并且了解单例模式的都知道,它有饿汉式、懒汉式、DCL(双重锁判断)、静态内部类以及枚举等多种写法。但说实话,在实际应用中,单例用到的并非不少。但做为设计模式的基本模式之一,咱们也有必要了解单例是否知足需求,例如线程是否安全,是否延迟加载,反射是否安全,序列化是否安全,这是本文重点关注的问题。
单例模式就是在应用的整个生命周期中只存在一个实例。它有不少好处,避免实例对象的重复建立,减小实例对象的重复建立,减小系统开销。例如spring容器中管理的Bean默认就是单例的。java
public class HungrySingleton implements Serializable{
private static HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return singleton;
}
}
复制代码
之因此implements Serializable(下同),是为了后面测试序列化是否安全的须要,通常状况不用加。面试
饿汉式在类加载时期就已经初始化实例,而咱们知道类加载是线程安全的,因此饿汉式是线程安全的。很明显,它不是延迟加载的,这也是饿汉式的缺点。经过下面的测试方法1,饿汉式不是反射安全的,由于经过反射构造方法产生了两个实例。经过测试方法2,饿汉式也不是序列化安全的。spring
测试方法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:bash
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:ui
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());
}
复制代码
运行结果:
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看来有这么三步:
一、为对象分配存储空间
二、初始化对象
三、将singleton引用指向第一步中分配的内存地址
第2步和第3步可能存在重排序。假设线程A按二、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()源码中,有这么几行,是枚举类型直接抛异常了。最后枚举单例也是序列化安全的,能够本身测试一下。
经过以上测试,了解了五种单例模式各有优缺点,没有说哪一种单例模式最好,只有知足需求的才是最合适的。