0%

java | double check lock 问题

这个例子非常经典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;

public static Singleton getInstance() {
if(INSTANCE == null) { // t2,这里的判断不是线程安全的
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
// 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

double check lock 是一种提升多线程之间效率的表现。

如果只是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;

public static Singleton getInstance() {
synchronized(Singleton.class) {
// 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}

那么,每次进来都需要进入 synchronized ,非常消耗资源。所以,在外层加了 INSTANCE 判断。

这样看似更合理,并且更快,但是,这样反而是线程不安全的。

这是因为 INSTANCE = new Singleton(); 这个代码的字节码是 4 个。

1
2
3
4
17: new 			#3 		// class test/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Ltest/Singleton;
  • 17 表示创建对象,将对象引用入栈
  • 20 表示复制一份对象引用,引用地址
  • 21 表示利用一个对象引用,调用构造方法初始化对象
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

步骤 2124 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

考虑这样一个情况

  • t1 刚进入,此时 INSTANCEnull ,其初始化对象
    • 由于重排问题,其先执行了 24 ,就是给 INSTANCE 付给了一个对象引用,但是,此时对象并没有创建
    • CPU 轮训
  • t2 进入,发现 INSTANCE 有了对象引用,开始使用 INSTANCE,但是,此时 INSTANCEnull

所以,我们要进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;

public static Singleton getInstance() {
if(INSTANCE == null) { // t2,这里的判断不是线程安全的
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
// 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

使用 volatile 修饰 INSTANCE,这是因为 volatile 具有写屏障和读屏障。可以保证 INSTANCE = new Singleton(); 执行的有序性,使其按照 17 - 20 - 21 - 24 的顺序执行。

请我喝杯咖啡吧~