如何构建一个正确的单例

单例模式是设计模式里面最常见的,也是在面试过程中面试官最容易考到的,通过单例模式还可以引申到其他的一些并发问题。今天我们来聊聊如何正确的构建一个单例。

dragonboat_ZH-CN0697680986_1920x1080

首先我们来写一个最简单的单例,该单例只能使用在单线程的情况下。

1
2
3
4
5
6
7
8
9
10
public class UnsafeLazyInitialization {
private static Resource resource;

public static Resource getInstance() {
if (resource == null) {
resource = new Resource(); // 多线程情况下是不安全的发布
}
return resource;
}
}

我们来分析一下这个单例如果放到多线程模式下会有什么问题,如果线程 A 和 B 同时到达 if (resource == null) 的时候(或者是由于可见性的原因,线程 B 没有看到线程 A 已经对引用赋值了),此时判断逻辑都为 true 于是就会创建 Resource 对象,两个线程创建了两个对象(如果有多个线程,可能会创建更多的对象),不符合单例的要求。相对这个问题而言,这里还隐藏了更危险的问题,由于指令的重排序的原因,可能导致一些线程能够获取这个单例,但是单例对象还在初始化,内部处于一个不确定的状态。

饿汉单例(静态类模式)

下面我们改进一下这个单例,让其变为一个线程安全的单例。

1
2
3
4
5
6
public class EagerInitialization { // 饿汉模式
private static Resource resource = new Resource(); // 通过类加载过程,提前加载单例,避免线程竞争
public static Resource getResource() {
return resource;
}
}

上面的代码如何做到的线程安全,是由于利用了静态初始化器。

静态初始化器是由 JVM 再类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于 JVM 将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个累已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。

懒汉单例 (静态类模式)

上面的模式展示了如何使用 Java 本身类加载的机制来保证单例的,使用同样的方式也可以构建一种懒汉模式的单例。

1
2
3
4
5
6
7
8
public class ResourceFactory {
private static class ResourceHolder {
public static Resource resource = new Resource();
}
public static Resource getResource() {
return ResourceHolder.resource;
}
}

双重检测锁 Double Checked Locking

在江湖上还传说着一种既高效又线程安全的单例写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不要这么做
public class DoubleCheckedLocking {
private static Resource resource;

public static getInstance() {
if (resource == null) {
synchronized(DoubleCheckedLocking.class) {
if (resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}

上面的 DCL 真正的问题在于在没有同步的情况下读取一个共享对象。可见性问题会造成有些线程可能看到失效的值,这个值可能是一个 null ,更加糟糕的情况下,这个值可能是一个不确定的值,因为该对象还没有初始化完成就发布了。

上面的例子可以改写成以下代码,通过使用 volatile 来保证共享对象的可见性

1
2
3
4
public class DoubleCheckedLocking {
private static volatile Resource resource;
.....
}

然而,DCL 的这种使用方法已经被广泛地废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及 JVM 启动很慢)已经不复存在了,因此它不是一种高效的优化措施。延迟初始化能带来同样的优势,并且更容易理解。

枚举类模式实现枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface MySingleton {
void doSomething();
}

public enum Singleton implements MySingleton {
INSTANCE {
@Override
public void doSomething() {
System.out.println("complete singleton");
}
};

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

effective java 推荐使用枚举来做单例

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

使用枚举来做单例的优势是:写法简单,单例线程安全由 JMM 提供、反序列化得到的对象也能保证是同一个。

单例线程安全:如果反编译单例的代码之后,会发现是一些 static 的属性和代码块。

反序列化的保障:

Java的序列化机制针对枚举类型是特殊处理的。简单来讲,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象。

总结

推荐使用枚举去实现单例模式!