# 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/)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chanshiyu.gitbook.io/blog/hou-duan/gao-xiao-bian-cheng/04-dan-li-mo-shi-de-ji-zhong-shi-xian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
