单例的7种实现方式和破坏单例的序列化和反射问题及解决方法
七种单例模式
是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
一、 饿汉式(使用全局的静态常量实现,线程安全)
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这种方式基于classloder机制避免了多线程的同步问题。instance在类加载的时候就实例化了,所以不是懒加载。
二、 饿汉式(使用静态代码块实现,线程安全)
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种方式也是基于classloder机制避免了多线程的同步问题。instance在类加载的时候就实例化了,所以不是懒加载。
三、 懒汉式(内部方法实现,非线程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
四、 懒汉式(同步方法实现,线程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
五、懒汉式(同步代码块实现,线程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
六、双重检查(懒加载+双非空判断,线程安全)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
七 静态内部类(线程安全)
public class Singleton {
private Singleton (){}
//内部类
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static final Singleton getInstance() {
return SingletonHolder.instance;
}
}
这种方式也是基于classloder机制避免了多线程的同步问题。至于这个单例是否是懒加载。如果我们知道一个类的静态内部类何时加载的,就会知道。 一个类加载了,它的静态内部类不会被加载,除非我们调用这个类的变量或者方法,这个类才会加载。
所以instance实例不会因为Singleton类的加载而加载,我们只有调用getInstance()方法的时候才会去加载SingletonHolder类,从而实例化instance
八、 枚举(线程安全)
public enum Singleton {
//Singleton的单实例
instance;
//测试使用
public void getMess(){
System.out.println("ok");
}
}
这种方式是《Effective Java》作者推荐的方式,他能够避免多线程同步问题。
枚举可解决线程安全问题
定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。
通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac
的编译之后,会被转换成形如public final class T extends Enum
的定义。
而且,枚举中的各个枚举项同事通过static
来定义的。如:
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后代码为:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
static
类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。他是随着类的加载而加载的,所以它不是懒加载。
破坏单例模式的反序列化和反射
我们创建完成的单例模式,其实并不是那么完美。 上面列举的7中单例模式,除了枚举单例模式
外,其他的6中都有可能出现,使用反序列化和反射,从而破坏单例模式,使我们的单例不在单例了。
反序列化破坏单例问题
以 使用全局的静态常量实现的饿汉式为例,看看反序列化破坏单例的情况。
如果测试反序列化问题,那么我们的类必须实现标记接口Serializable,只有实现Serializable接口的类的实例,才能进行序列化写入文件,才能造成反序列化问题。
public class Singleton implements Serializable {
private Singleton(){}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
}
进行测试:
@Test
public void testSynchronized() throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
// 将HungerSingletons实例写入文件
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Serializable"));
outputStream.writeObject(singleton);
outputStream.flush();
outputStream.close();
//读取文件中的实例
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("Serializable"));
Singleton serializableSingleton = (Singleton)inputStream.readObject();
inputStream.close();
//判断singleton和反序列化的实例serializableSingleton的内存地址是否相同
System.out.println("singleton 是否等于 serializableSingleton " + (singleton == serializableSingleton));
//查看两个实例的内存地址
System.out.println("singleton内存地址 " +singleton);
System.out.println("serializableSingleton内存地址 " +serializableSingleton);
}
测试结果: 我们从结果中可以看出,使用反序列化创建的实例和我们创建的实例,不是同一个实例。破坏的单例模式。
singleton 是否等于 serializableSingleton false
singleton内存地址 pattern.singleton.Singleton@15327b79
serializableSingleton内存地址 pattern.singleton.Singleton@470e2030
防止反序列化破坏单例
我们上面的的反序列化攻击的例子,使单例模式创建除了两个实例。出现这样的原因是因为我们inputStream.readObject()方法。
任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
解决方法: 在我们单例类中加入readResolve()让它直接返回我们自己创建的实例。
public class Singleton implements Serializable {
private Singleton(){}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
//添加此方法后,解决反序列化的问题
private Object readResolve(){
return instance;
}
}
测试结果: 再次测试,结果显示反序列化后,我们得到的实例依然是同一个实例,顺利的解决了反序列化问题。
singleton 是否等于 serializableSingleton true
singleton内存地址 pattern.singleton.Singleton@15327b79
serializableSingleton内存地址 pattern.singleton.Singleton@15327b79
反射破坏单例问题
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//使用单例模式的getInstance()创建对象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("验证是单例模式: " +(singleton == singleton2));
// 反射创建实例化对象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以获取私有构造函数。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//比较getInstance()方法创建的实例和反射创建实例对象是否是一个实例
System.out.println("reflectSingleton是否等于singleton: " + (reflectSingleton == singleton));
}
结果
验证是否单例模式: true
reflectSingleton是否等于hungerStaitcConstantSingleton: false
reflectSingleton pattern.singleton.Singleton@4ee285c6
singleton pattern.singleton.Singleton@621be5d1
防止反射破坏单例
对私有构造方法的改造,实现防止反射破坏单例
反射是调用我们的私有构造方法,来创建实例的。我们的防护,可以围绕私有构造方法实现。
我们可以在私有构造方法内进行判断,如果进行已经有实例了,那么就抛出异常。
我们使用 饿汉式(使用全局静态常量实现的单例)来进行测试。
public class Singleton implements Serializable {
/**
* 反射通过私有构造创建实例
* 在私有构造方法中判断是否已经创建实例,如果创建,抛出异常
*/
private Singleton(){
if (instance != null) {
throw new RuntimeException("单例模式, 不能重复创建实例");
}
}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
}
使用测试类测试
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//使用单例模式的getInstance()创建对象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("验证是否单例模式: " +(singleton == singleton2));
// 反射创建实例化对象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以获取私有构造函数。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//比较getInstance()方法创建的实例和反射创建实例对象是否是一个实例
System.out.println("reflectSingleton是否等于singleton: " + (reflectSingleton == singleton));
}
测试结果:
我们可以看见,我们先用正常的情况获取实例,再用反射创建实例时,抛出了异常。如果我们先用反射创建实例,再用正常情况获取实例。 依然会报错。这种情况就解决了反射破解单例的情况。
我们看另一种情况 用懒汉式来测试。
我们测试时,就不先正常获取实例,再反射获取实例。 我们直接先反射获取再正常获取。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
测试类:
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
// 反射创建实例化对象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以获取私有构造函数。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//使用单例模式的getInstance()创建对象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("验证是否单例模式: " +(singleton == singleton2));
//比较getInstance()方法创建的实例和反射创建实例对象是否是一个实例
System.out.println("reflectSingleton是否等于hungerStaitcConstantSingleton: " + (reflectSingleton == singleton));
System.out.println("reflectSingleton " + reflectSingleton);
System.out.println("singleton " + singleton);
}
结果:
验证是否单例模式: true
reflectSingleton是否等于hungerStaitcConstantSingleton: false
reflectSingleton pattern.singleton.Singleton@4ee285c6
singleton pattern.singleton.Singleton@621be5d1
通过结果我们发现,这次我们又出现了反射破坏单例的情况。
总结:
我们根据这两种情况可以看出对私有构造方法的改造,实现防止反射破坏单例这种情况并不是通用了。它并不能解决所有单例模式的反射攻击的情况。
使用范围: 对私有构造方法的改造,实现防止反射破坏单例这种方式,只能针对全局静态常量创建实例或者静态代码块创建实例的这种非懒加载情况(第一种单例模式、第二种单例模式和第七中单例模式都适用)。 java基础的时候,就学过像全局静态常量和静态代码块,它们是属于类的,它们会随着类的加载而加载。所以不管我们使用什么方法,他都会在类被加载的时候,就创建好实例,所以我们即使先使用反射的方式创建实例,它也会先我们一步将类加载进来,并创建实例,是我们使用反射获取实例失败。
虽然第七种静态内部类的方法是懒加载的,但是实际上,静态内部类中创建实例依然是随着类的加载而加载的,所以它适用改造私有构造方法的解决方法。
如果我们使用的单例模式不是懒加载,那么这个对私有构造方法的改造,实现防止反射破坏单例这种方法就不适用。(单例模式的第三种,第四种,第五种,第六种都不适用)
总结:
从这篇文章中我们也看到了单例模式的七种实现方式和优缺点。以及单例模式中可能存在的问题。
我在下面的表格中列举了这七种实现方式的优缺点和他们是否存在反序列化和反射攻击的情况。
懒加载 | 线程安全 | 防止反序列化 | 防止反射 | |
---|---|---|---|---|
饿汉式 (全局静态常量实现) | 否 | 是 | 否 | 否 |
饿汉式 (静态代码块实现) | 否 | 是 | 否 | 否 |
懒汉式 (内部方法实现) | 是 | 否 | 否 | 否 |
懒汉式 (同步方法实现) | 是 | 是 | 否 | 否 |
懒汉式 (同步代码块实现) | 是 | 是 | 否 | 否 |
双重检查 | 是 | 是 | 否 | 否 |
静态内部类 | 是 | 是 | 否 | 否 |
枚举 | 否 | 是 | 是 | 是 |
Hollis的为什么我墙裂建议大家使用枚举来实现单例。
Wenlong_L的设计模式之单例模式六(防反射攻击)
这边文章并非是我原创的,写这篇文章主要也是想留个底。其中参考了大量其他博主的博文。这些博主写的都很详细比我好多了,大家可以看看。
因为这篇文章,我改了很多次中间可能有不对的地方,如果大家发现,麻烦给我指出,谢谢。