单例模式

单例模式是相对比较简单的设计模式,也是 Java 中常用常见的设计模式。

饥饿模式(线程安全)

1
2
3
4
5
6
7
8
9
10
public class Singleton {

private static Singleton instance = new Singleton();

private Singleton() {}

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

饥饿模式也叫饿汉式,由 static 保证对象实例只在类加载时创建一次,说它线程安全是因为类的初始化是由 ClassLoader 完成的,而加载类的 loadClass 方法是通过 synchronized 修饰的。对于大对象来说,这种方式还是比较浪费内存的,因此可以通过懒加载的方式来创建。

懒加载(线程不安全,不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton {

private static LazySingleton instance;

private LazySingleton() {}

public static LazySingleton lazySingleton() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

懒加载也叫懒汉模式,只有在真正要用到对象实例时才创建对象。但是这带来了新的问题,即由于线程不安全导致单例可能会变成多例,比较简单的处理方式就是加入同步。

线程安全的懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton {

private static LazySingleton instance;

private LazySingleton() {}

public static synchronized LazySingleton lazySingleton() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

加入同步固然能够保证线程安全,但是为了性能考量,又引入了一种新的方式:双重检查锁模式。

双重检查锁模式(只为了演示,不推荐使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {

private static LazySingleton instance;

private LazySingleton() {}

public static LazySingleton lazySingleton() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}

双重检查锁模式首先检查实例是否被创建,如果已创建则返回;没有则进入临界区,再次检查实例是否被创建,没有则会创建对象。第一重检查是为了提高性能,通过空判断避免每次都进入临界区,只有在需要创建对象时才进入。这个优化看起来是很完美的,然而实际上它是存在隐患的。

JVM 在创建对象时会分为如下三步:

1
2
3
4
5
6
7
8
// 分配内存空间
memory = allocate();

// 执行构造方法,初始化对象
ctorInstance(memory);

// 将 instance 指向分配的内存空间
instance = memory;

可以看到,第二行和第三行代码依赖于第一行的结果,但是第二行和第三行代码之间不存在依赖性,并且我们知道 synchronized 的有序性是指加锁之前必须解锁,它无法做到像 volatile 一样禁止指令重排序,所以在代码 instance = new LazySingleton() 部分可能会发生以下情况:

1
2
3
4
5
6
7
8
9
10
11
// Allocate memory for Singleton object.
// 为对象分配内存
memory = allocate();

// Note that instance is now non-null, but has not been initialized.
// 将 instance 指向分配的内存空间,此时还没完成实例的初始化
instance = memory;

// Invoke constructor for Singleton passing instance.
// 传递实例,执行构造方法
ctorSingleton(instance);

这在一些 JIT 编译器上是会真实发生的,这种重排序带来的问题就是如果一个线程在执行完内存分配以及部分初始化后,其他线程访问获取实例的方法,此时的实例对象已经非空了,所以该线程将会获取到一个还未初始化完成的对象。为了避免这种问题的发生,可以使用 volatile 改造。

优化双重检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {

private static volatile LazySingleton instance;

private LazySingleton() {}

public static LazySingleton lazySingleton() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}

先行发生原则中指出,对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。因此使用 volatile 可以保证实例变量在未完全初始化之前不会被读取。

Initialization On Demand Holder

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

private Singleton() {}

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

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

这种方式使用内部类来做到延迟加载对象,同时在初始化这个内部类的时候,由 JLS(Java Language Sepcification)保证这个类的线程安全,详情可以查看我在参考中附加的链接。

枚举

1
2
3
public enum Singleton {
SINGLETON
}

使用枚举不仅能够避免多线程同步问题,还能防止反射和反序列化重新创建新对象,被《Effective Java》的作者 Josh Bloch 所提倡,详情可以查看我在参考中附加的链接。

单例的三个问题

反射会破坏单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {

private Singleton() {}

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

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

public static void main(String[] args) throws Exception {
Class<?> type = Singleton.class;
Constructor<?> constructor = type.getDeclaredConstructor();
// 暴力获取权限
constructor.setAccessible(true);
Singleton instance1 = (Singleton) constructor.newInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // 返回 false,说明不是同一个对象
}
}

目前笔者所知的处理手段就是使用枚举实现单例,可以避免反射攻击。

序列化会破坏单例

如果单例类实现了 java.io.Serializable 接口,那么这个类的实例就可能被序列化破坏(能够通过反序列化来复原多个实例从而变成多例,原因是序列化会通过反射调用无参数的构造方法来创建一个新的对象)。解决方法就是在单例类中添加一个 readResolve 方法始终返回同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LazySingleton implements java.io.Serializable {

private static volatile LazySingleton instance;

private LazySingleton() {}

public static LazySingleton LazySingleton() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}

private Object readResolve() {
return instance;
}
}

使用不同的类加载器加载会破坏单例

如果单例类由不同的类加载器加载,可能就会存在多个单例类的实例,比如一些 Servlet 容器对每个 Servlet 使用完全不同的类加载器,如果 Servlet 中使用单例类,就会造成每个 Servlet 使用的都不是同一个单例对象。解决方法留待以后探究。

参考

Java 单例模式中双重检查锁的问题

双重检查锁定模式

Updates for J2SE 5.0 (aka 1.5, Tiger)

Initialization-on-demand holder idiom

深度分析 Java 的枚举类型—-枚举的线程安全性及序列化问题

单例与序列化的那些事儿