0%

设计模式 - 单例模式

单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。——《维基百科》

单例模式在开发中应用非常广泛,某些对象的构造可能会非常耗费资源,在整个软件周期中我们只想让其存在一个实例,或者作为全局通用对象,我们希望它在任意地方的访问取得一致的结果,那就可以使用单例模式,像Servlet,Spring中的Bean默认都是单例的。单例模式的实现方式有好几种,本文主要记录在Java下单例模式的具体实现方式。

饿汉式

1
2
3
4
5
6
7
8
9
10
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 Singleton {
private static Singleton INSTANCE;

private Singleton() {
}

public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

懒汉式是在第一次获取实例的时候进行单例初始化,对获取方法加synchronized同步锁,保证了线程安全,也存在反射和反序列化的问题。

双重检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private volatile static Singleton INSTANCE;

private Singleton() {
}

public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

大名鼎鼎的DCL,饿汉式的优化版本,使用双重检查锁保证了线程安全,同时降低了在多线程环境下的加锁竞争,volatile关键字的应用典范(变量多线程可见性)。

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}

静态内部类的方式较之前的几种都有了不小的提升,因为基于类的加载机制进行初始化,防止了线程安全的问题,同时只有在调用获取实例方法时才会触发第一次加载,也实现了懒加载机制,写法也比较简单,算是一种比较良好的实现方式了。

枚举

1
2
3
public enum Singleton {
INSTANCE;
}

嗯,枚举实现单例就是这么简单且纯粹。同时基于对枚举的特殊处理机制,保证了线程安全,也规避了反射和反序列化对单例带来的破坏,这是我最常用来实现单例的方式。

反射和反序列化对单例的破坏

反射破坏单例模式

以静态内部类为例,反射获取单例类的构造器,变更构造器的访问权限之后可以直接实例化,生成与单例接口返回不一致的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Singleton {
private Singleton() {
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static void main(String[] args) throws Exception {
// 反射并实例化
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton refObject = constructor.newInstance();
System.out.println("singleton:" + Singleton.getInstance().hashCode());
System.out.println("refObject:" + refObject.hashCode());
System.out.println("equals:" + (refObject == Singleton.getInstance()));
}
}
// 输出
// singleton:460141958
// refObject:1163157884
// equals:false

想要阻止反射对单例的破坏,可行的方法是在构造器中抛出异常,禁止反射对构造器的调用。

1
2
3
4
5
6
private Singleton() {
// 禁止反射调用构造器
if (SingletonHolder.INSTANCE != null) {
throw new RuntimeException("not allow");
}
}

反序列化破坏单例

还是以静态内部类为例,对象实现序列化接口后,对其进行序列化,把它写入文件中,然后再反序列化读出来,生成与单例接口返回不一致的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Singleton implements Serializable {
private Singleton() {
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static void main(String[] args) throws Exception {
// 先序列化然后再反序列化
String path = System.getProperty("user.dir") + "/file";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(path))) {
out.writeObject(Singleton.getInstance());
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(path))) {
Singleton inObject = (Singleton) in.readObject();
System.out.println("singleton:" + Singleton.getInstance().hashCode());
System.out.println("inObject:" + inObject.hashCode());
System.out.println("equals:" + (inObject == Singleton.getInstance()));
}
}
}
// 输出
// singleton:1735600054
// inObject:1452126962
// equals:false

阻止反序列化对单例的破坏,可以禁止序列化,禁止反序列化,也可以重写readResolve()方法,使其返回单例对象。

禁止序列化:

1
2
3
4
private void writeObject(ObjectOutputStream oos) throws IOException {
// 阻止对象序列化
throw new RuntimeException("not allow");
}

禁止反序列化:

1
2
3
4
private void readObject(ObjectInputStream ois) {
// 阻止对象反序列化
throw new RuntimeException("not allow");
}

也可以在构造器中禁止反序列化,因为反序列化时,会利用反射调用类中的构造器:

1
2
3
4
5
6
private Singleton() {
// 禁止反射调用构造器
if (SingletonHolder.INSTANCE != null) {
throw new RuntimeException("not allow");
}
}

重写readResolve()方法,直接返回单例对象:

1
2
3
private Object readResolve() {
return SingletonHolder.INSTANCE;
}

枚举单例为什么可以防止反射和序列化的破坏

这是Constructor中生成实例的方法,可以看到当类对象是枚举时,会直接抛出IllegalArgumentException异常,禁止反射生成枚举的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Constructor的newInstance方法
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
// 省略部分代码
// 当实例化类是枚举时,抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
// 省略部分代码
T inst = (T) ca.newInstance(initargs);
return inst;
}

在ObjectInputStream类中,反序列化枚举对象时,会调用readEnum()方法,然后调用Enum类的valueOf()方法直接返回枚举的实例,所以可以保证实例的唯一性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ObjectInputStream的readEnum方法
private Enum<?> readEnum(boolean unshared) throws IOException {
// 省略部分代码
if (cl != null) {
try {
@SuppressWarnings("unchecked")
// 调用Enum的valueOf方法
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
// 省略部分代码
return result;
}
1
2
3
4
5
6
7
8
9
10
11
// Enum的valueOf方法
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}