一、基本介绍

1.1 概念

单例模式,即单个实例的模式,开发中非常常见,比如全局对象管理、工具类等基本都会使用单例模式。

1.2 优缺点

优点:

  • 节约资源:不用频繁创建对象,减少了系统开销。
  • 方便访问:提供全局统一访问点,便于调用。

缺点:

  • 由于单例对象一般都是长生命周期对象,使用不当很有可能导致内存泄漏(单例对象持有短生命周期对象导致其无法被回收)。

二、实现

1.1 饿汉式

听这个名字可能一头雾水,可以这样想:类似于一个”饥饿”的人,在程序启动时就要马上吃饭(创建实例),不管后续是否会真正需要使用这个实例
实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
// 类加载的时候就new一个对象
private static Singleton INSTANCE = new Singleton();

// 私有构造器,防止外部创建实例
private Singleton() {}

// 全局统一访问点
public static Singleton getInstance() {
return INSTANCE;
}
}

饿汉式是最简单的一种单例实现方式,但是同时也带来了资源浪费的问题,上面也说了,不管后续有没有使用到该类实例,实例都已经被创建了,这显然是不太优雅的。

1.2 懒汉式

由于饿汉式不够优雅,懒汉式应运而生,这个名字更好理解,可以想象成这个人很”懒”,只有在饿得不行的时候才吃返(创建对象)。
实现如下:

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

private static Singleton INSTANCE;

private Singleton() {}

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

这样就优雅不少了,只有在真正需要的时候,这个单例对象实例才会被创建。
但是,优雅仅限于单线程的情况,在多线程环境下,上面方法并不安全。
比如,此时有AB两线程同时进入getInstance()方法,并且此时INSTANCE实例之前还未被创建过,就会出现如下情况:

  1. 线程A首先判断INSTANCEnull,然后开始创建新的实例。
  2. 在线程A还未完成实例化之前,线程B也判断INSTANCEnull,然后也开始创建新的实例。
  3. 最终,线程A和线程B分别创建了各自的实例,导致系统中存在多个实例,违背了单例模式的初衷。

1.2.1 懒汉式优化①

不就是多线程嘛,加锁!由此,你得出了第一种优化方案,给getInstance()方法加锁

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

private static Singleton INSTANCE;

private Singleton() {}

// 保证每次只有一个线程进入getInstance()方法
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

这样不就好了吗,即实现了懒加载,又实现了线程安全。
但是,性能呢?上面这个锁明显属于粗粒度锁,以后每次访问都需要先获取锁才能进入getInstance()方法,显然是一笔巨大的性能开销。

1.2.2 懒汉式优化②

思考之后,你得出了第二种优化方案,即只对方法中的创建实例的局部代码加锁,也就是降低锁的粒度:

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

private static Singleton INSTANCE;

private Singleton() {}

public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) { // --> 位置1
INSTANCE = new Singleton(); // --> 位置2
}
}
return INSTANCE;
}
}

这样的话,每次线程进来会先获取锁,然后只有一个线程去执行同步代码块,创建INSTANCE实例。
但是,上面这样写对吗?不妨举个例看一下:

  1. 线程AB同时进入getInstance()方法,由于是第一次进入,INSTANCE还未实例化,二者都到了位置1
  2. 假设线程A抢到了锁,到达位置2,创建完对象之后退出同步代码块释放锁。
  3. 接下来线程B拿到锁,到达位置2,此时线程B并不知道A已经创建了实例,仍会创建另一个对象实例。
  4. 最终线程A和线程B分别创建了各自的实例,导致系统中存在多个实例,完了,又搞回去了。

1.2.3 懒汉式优化③

不过这次简单,再加个判空就行了嘛,于是有了双重检查锁定(double-checked locking)

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

private static Singleton INSTANCE;

private Singleton() {}

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

nice!大功告成!欸等等,怎么第一个if那里有个警告?点开灯泡看一下:

哦?IDE 已经发现我们写出了一段双重检查锁定的代码,并贴心的给出了提示信息。
按照提示信息,我们在INSTANCE变量声明时加上volatile

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

private static volatile Singleton INSTANCE;

private Singleton() {}

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

警告消失!至此,终于是完成了单例模式的懒汉式实现。

最后留下一个问题,就是volatile在这里究竟是起到了防止指令重排,还是可见性的效果?还是两者都有?网络上对此也是各种说法,我个人更倾向于可见性一点,欢迎留言一起讨论!