# 04 单例模式的几种实现

单例模式是一种对象创建模式，它用于产生一个对象的具体实例，它可以确保系统中一个类只产生一个实例。Java 里面实现的单例是一个虚拟机的范围，因为装载类的功能是虚拟机的，所以一个虚拟机在通过自己的 ClassLoad 装载实现单例类的时候就会创建一个类的实例。

单例模式的优点：

* 对于频繁使用的对象，可以省略创建对象所花费的时间，这对于那些重量级对象而言，是非常可观的一笔系统开销；
* 由于 new 操作的次数减少，因而对系统内存的使用频率也会降低，这将减轻 GC 压力，缩短 GC 停顿时间。

单例模式的核心在于通过一个接口返回唯一的对象实例。首要的问题就是要把创建实例的权限收回来，让类自身来负责自己类的实例的创建工作，然后由这个类来提供外部可以访问这个类实例的方法。

## 饿汉式

```java
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInsatnce() {
        return instance;
    }
}
```

饿汉式提前实例化，没有懒汉式中多线程问题，但无法对 instance 实例做延时加载，单例会在加载类后一开始就被初始化，保证该实例只有一个，不管是否调用 `getInstance()` 都会存在一个实例在内存中。

此外饿汉式的创建方式在一些场景中将无法使用：譬如 Singleton 实例的创建是依赖参数或者配置文件的，在 `getInstance()` 之前必须调用某个方法设置参数给它，那样这种单例写法就无法使用了。

## 懒汉式

```java
public class Singleton {
    private static Singleton instance;
    private Singleton() {}

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

懒汉式实现了实例延时加载，但是需要添加 `synchronized` 来保证线程安全。但这样做并不高效，因为在任何时候只能有一个线程调用 `getInstance()` 方法。

## 双重检验锁

双重检验锁模式（double checked locking pattern），是一种使用同步块加锁的方法，可以看作是上面懒汉式的改进版。双重检查锁会有两次检查 `instance == null`，一次是在同步块外，一次是在同步块内。第一个 if 可以提升效率，为什么在同步块内还要再检验一次？因为可能会有多个线程一起进入同步块外的 if，如果在同步块内不进行二次检验的话就会生成多个实例了。

```java
public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getSingleton() {
        if (instance == null) {                         // Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {                 // Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
```

双重检验锁看起来很完美，但可惜它有问题。主要在于 `instance = new Singleton()` 这句，这并非是一个原子操作，事实上在 JVM 中这句话大概做了下面 3 件事情：

1. 给 instance 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量
3. 将 instance 对象指向分配的内存空间（执行完这步 instance 就为非 null 了）

但是在 JVM 的即时编译器中存在**指令重排序**的优化。也就是说上面的第 2 步和第 3 步的顺序是不能保证的，最终的执行顺序可能是 `1-2-3` 也可能是 `1-3-2`。如果是后者，则在 3 执行完毕、2 未执行之前，被线程二抢占了，这时 instance 已经是非 null 了（但却没有初始化），所以线程二会直接返回 instance，然后使用，然后顺理成章地报错。

这时需要将 instance 变量声明成 `volatile` 就可以了：

```java
public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}

    public static Singleton getSingleton() {
        if (instance == null) {                         // Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {                 // Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance ;
    }
}
```

使用 `volatile` 的主要原因是其一个特性：**禁止指令重排序优化**。也就是说，在 `volatile` 变量的赋值操作后面会有一个内存屏障（生成的汇编代码上），读操作不会被重排序到内存屏障之前。比如上面的例子，取操作必须在执行完 `1-2-3` 之后或者 `1-3-2` 之后，不存在执行到 `1-3` 然后取到值的情况。从「先行发生原则」的角度理解的话，**就是对于一个 `volatile` 变量的写操作都先行发生于后面对这个变量的读操作**（这里的“后面”是时间上的先后顺序）。

## 静态内部类

推荐使用静态内部类的方法，这种方法也是《Effective Java》上所推荐的。

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

    private Singleton() {}

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

这种写法使用 JVM 本身机制保证了线程安全问题；由于 `SingletonHolder` 是私有的，除了 `getInstance()` 之外没有办法访问它，因此它是**懒汉式**的；同时读取实例的时候不会进行同步，没有性能缺陷；也不依赖 JDK 版本。

## 枚举

同样也是《Effective Java》推荐的写法，它不仅能避免多线程同步问题，而且还能防止反序列化重新创建新的对象，但使用较少。

```java
public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
    }
}
```

调用：

```java
Singleton.INSTANCE.whateverMethod();
```

参考文章：\
[Java 编程设计模式-单例模式](https://www.ibm.com/developerworks/cn/java/j-lo-Singleton/index.html)\
[如何正确地写出单例模式](http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/)
