前言
关于单例模式,作为23种设计模式中最为常用的设计模式,单例模式并没有想象的那么简单。因为在设计单例的时候要考虑很多问题,比如线程安全问题、序列化对单例的破坏等。
有关单例的学习可以看单例模式的七种写法
一、一般单例模式痛点问题
要保证线程安全、序列化与反序列化安全、反射安全
二、其他单例模式与枚举对比
下面举一种线程安全的单例与枚举做对比
1、“双重校验锁”实现单例:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
2、枚举实现单例:
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
相比之下,你就会发现,枚举实现单例的代码会精简很多。
上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化与反射破坏单例的问题。
3、“双重校验锁”反射攻击
public class SingletonTest {
@Test
public void singletonTest() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Singleton singleton1 = Singleton.getSingleton();
Singleton singleton2 = Singleton.getSingleton();
System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
Constructor<Singleton> constructor= Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton3 = constructor.newInstance();
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
}
}
三、分析枚举单例如何保证线程安全、序列化与反序列化安全、反射安全
1、可解决线程安全问题
上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?
其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。
那么,“底层”到底指的是什么?
然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后代码内容如下:
- 该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,
- 我们看到这个类中属性都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。
如果大家对第二点有疑问可以详细看下面的文章这里不做展开分析
分析Java的ClassLoader机制(源码级别)
2、序列化与反序列化安全
1、“双重校验锁”实现单例反序列化攻击
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package enums;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import org.junit.Test;
public class SingletonTest implements Serializable {
public SingletonTest() {
}
@Test
public void singletonSerializeTest() throws IOException, ClassNotFoundException {
Singleton s = Singleton.getSingleton();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.obj"));
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton s1 = (Singleton)ois.readObject();
ois.close();
System.out.println("序列化前后两个是否同一个:" + (s == s1));
}
}
源码分析:
跟踪进入ois.readObject()
,会进入ObjectInputStream.readObject0()
方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。
重点关注case TC_OBJECT
如下所示:
进入readOrdinaryObject
方法,重点关注desc.isInstantiable() ? desc.newInstance() : null;
。如下所示:
有obj =desc.isInstantiable() ? desc.newInstance() : null;
可知,
- isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
- desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?
答:序列化会通过反射调用无参数的构造方法创建一个新的对象。
那么,如何防止序列化/反序列化破坏单例模式。
有上图代码desc.hasReadResolveMethod()
方法,跟踪代码如下:
- hasReadResolveMethod:如果实现了serializable 或者externalizable接口的类中包含readResolve则返回true
- invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
可知如果代码包含有readResolve方法则会取该类方法的对像,实现如下:
package enums;
import java.io.Serializable;
/**
* @Description: “双重校验锁”实现单例:
*/
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve(){
return singleton;
}
}
2、枚举单例反序列化
public class SingletonTest implements Serializable {
@Test
public void enumSingletonSerializeTest() throws IOException, ClassNotFoundException {
EnumSingleton singleton1=EnumSingleton.INSTANCE;
System.out.println("枚举序列化前singleton1 :"+singleton1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
EnumSingleton singleton2 = (EnumSingleton)ois.readObject();
ois.close();
System.out.println("枚举序列化后singleton2 :"+singleton2);
System.out.println("枚举序列化前后两个是否同一个:"+(singleton1==singleton2));
}
}
重点关注case TC_ENUM
如下所示:
进入readEnum
方法,重点关注Enum.valueOf
方法。如下所示:
enumConstantDirectory()
是Class的方法,其本质是从Class.java的enumConstantDirectory
属性中获取。代码如下:
也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。
3、反射安全
public class SingletonTest implements Serializable {
@Test
public void enumSingletonreFlectTest() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton singleton1=EnumSingleton.INSTANCE;
EnumSingleton singleton2=EnumSingleton.INSTANCE;
System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
Constructor<EnumSingleton> constructor= EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton3= constructor.newInstance();
System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
}
}
可以发现是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性:
枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package enums;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import org.junit.Test;
public class SingletonTest implements Serializable {
@Test
public void enumSingletonreFlectTest2() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
EnumSingleton singleton2 = EnumSingleton.INSTANCE;
System.out.println("正常情况下,实例化两个实例是否相同:" + (singleton1 == singleton2));
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, Integer.TYPE);
constructor.setAccessible(true);
EnumSingleton singleton3 = (EnumSingleton)constructor.newInstance();
System.out.println(singleton1 + "\n" + singleton2 + "\n" + singleton3);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (singleton1 == singleton3));
}
}
继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,抛出异常说是不能够反射,我们看下Constructor类的newInstance方法源码:
请看红框标注的源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
四、总结
枚举单例优点:
- 枚举写法简单
- 保证线程安全、序列化与反序列化安全、反射安全;
Joshua Bloch大神在《Effective Java》中明确表达过的观点:
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
所以可以尝试用一下。