一般的单例模式

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

private Singleton() {
}

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

但是在多线程情况下,可能会导致多个实例:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 检查到uniqueSingleton为空
T3 初始化对象A
T4 返回对象A
T5 初始化对象B
T6 返回对象B

这种场景,就会创建两次对象。

加锁

代码如下:

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

private Singleton() {
}

public synchronized Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}

这种写法也存在问题,加锁的粒度太大了,只有在创建对象时才需要加锁,后续获取对象时并不需要加锁。

双重锁检查

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

private Singleton() {
}

public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
// 可能存在问题
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}

这样写,顺序就变成下面这样:

  1. 先判断对象是否存在,不存在则加锁。
  2. 加完锁之后再次判断对象是否存在。
  3. 不存在则创建。

双重加锁是因为如果多个线程同时通过了第一次判断,那么这几个线程中会有一个线程加锁成功,然后创建对象,后续线程在获取锁之后,就不用再继续创建对象。

存在的隐患

在实例化一个对象时,可以分为以下步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

但是编译器在为了执行速度,可能会进行指令重排序,那么顺序就有可能如下:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

现在考虑如下场景:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 获取锁
T3 第二次检查uniqueSingleton为空
T4 uniqueSingleton分配空间
T5 uniqueSingleton指向内存空间
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(此时对象还未完成初始化)
T8 初始化将uniqueSingleton

这种情况下,线程B就访问到了一个还未初始化完成的对象。

解决办法

使用volatile关键字禁止指令重排序。

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

private Singleton() {
}

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