单例模式的破坏与改进

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

单例的特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

单例模式的写法

可以参考这篇文章 单例模式的七种写法

破坏单例模式

反射

先来看一种常见的单例写法(饿汉模式):

1
2
3
4
5
6
7
8
9
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}

public static Singleton getInstance() {
return instance;
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestSingleton {
public static void main(String[] args) throws Exception {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);

Class clazz = Singleton.class;
Constructor cons = clazz.getDeclaredConstructor(null);
cons.setAccessible(true);
Singleton singleton3 = (Singleton) cons.newInstance(null);
System.out.println(singleton1 == singleton3);
}
}

运行结果:

1
2
true
false

结论:

通过反射可以破坏单例的属性,把它的构造方法设置成可访问的,然后去生成一个新的对象。

如何避免:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
if (instance != null) {
throw new UnsupportedOperationException("实例已存在,无法初始化!");
}
}

public static Singleton getInstance() {
return instance;
}
}

运行结果:

1
2
true
实例已存在,无法初始化!

序列号和反序列化

我们对饿汉模式的单例简单改动,让它可以序列化。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton implements Serializable {
private static final Singleton instance = new Singleton();
private Singleton() {
if (instance != null) {
throw new UnsupportedOperationException("实例已存在,无法初始化!");
}
}

public static Singleton getInstance() {
return instance;
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestSingleton {
public static void main(String[] args) throws Exception {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);

FileOutputStream fos = new FileOutputStream("a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton4 = (Singleton) ois.readObject();
System.out.println(singleton1 == singleton4);
}
}

运行结果:

1
false

结论:

通过对序列化后的对象进行反序列化得到的对象是一个新的对象,这就破坏了单例性。

如何避免:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton implements Serializable {
private static final Singleton instance = new Singleton();
private Singleton() {
if (instance != null) {
throw new UnsupportedOperationException("实例已存在,无法初始化!");
}
}

public static Singleton getInstance() {
return instance;
}

private Object readResolve() {
return instance;
}
}

原理就是,在 Singleton 中定义 readResolve 方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

单元素枚举类型

这是实现单例的最佳方式,内部已经避免了反射和序列化反序列化。

1
2
3
4
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}

总结

  1. 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
  2. 《Effective Java》作者 Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。

参考

Java 与单例模式
单例模式的七种写法