Poison

Double Checked Locking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.tianshuang;

public class Foo {

private Helper helper = null;

public Helper getHelper() {
if (helper == null) {
synchronized (this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}

}

以上的双重检查实现并不安全,原因为编译器可以重排序代码以使 Helper() 构造函数中的代码在写入 helper 变量后执行。如果发生了这样的情况,那么在构造线程写入 helper 变量之后,但在它实际完成对象构造之前,另一个线程可以在完成初始化之前出现并读取 helper,此时,该线程可能会看到对 helper 对象的非空引用,但会看到 helper 对象字段的默认值,而不是构造函数中设置的值。

什么意思呢,加入 Helper 类的源码如下:

1
2
3
4
5
6
7
8
9
10
11
package me.tianshuang;

public class Helper {

private int a;

public Helper() {
this.a = 1;
}

}

编译器可能将 this.a = 1 重排序至 Helper 类实例赋值给 helper 之后执行,导致其他线程看到的 helper 中的变量 a 的值为 0,即其他线程看到了未完全构造的对象。在 The “Double-Checked Locking is Broken” Declaration 中使用了 Symantec JIT 系统对这种场景进行了举例,如下代码 singletons[i].reference = new Singleton(); 编译后生成的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
0206106A   mov         eax,0F97E78h
0206106F call 01F6B210 ; allocate space for
; Singleton, return result in eax
02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference
; store the unconstructed object here.
02061077 mov ecx,dword ptr [eax] ; dereference the handle to
; get the raw pointer
02061079 mov dword ptr [ecx],100h ; Next 4 lines are
0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h

可以看出,申请对象空间后,先将对象地址赋值给 singletons[i].reference,再执行的构造函数中的初始化代码,这将导致其他线程看到未完全构造的对象。

在 JDK 5.0 及更高版本中扩展了 volatile 的语义,禁止了对 volatile 字段的指令重排序,通过将 helper 变量声明为 volatile 可以解决上面的问题,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.tianshuang;

public class Foo {

private volatile Helper helper = null;

public Helper getHelper() {
if (helper == null) {
synchronized (this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}

}

一个更高效的写法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package me.tianshuang;

public class Foo {

private volatile Helper helper = null;

public Helper getHelper() {
Helper localRef = helper;
if (localRef == null) {
synchronized (this) {
localRef = helper;
if (localRef == null) {
helper = localRef = new Helper();
}
}
}
return localRef;
}

}

本地变量 localRef 的引入能将方法的性能提升 40%,因为大多数时候,在 helper 变量已经初始化的情况下,localRef 的引入能够使 volatile 变量仅被访问一次。

比如在 Guava 的 RateLimiter.java 中含有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Can't be initialized in the constructor because mocks don't call the constructor.
@CheckForNull private volatile Object mutexDoNotUseDirectly;

private Object mutex() {
Object mutex = mutexDoNotUseDirectly;
if (mutex == null) {
synchronized (this) {
mutex = mutexDoNotUseDirectly;
if (mutex == null) {
mutexDoNotUseDirectly = mutex = new Object();
}
}
}
return mutex;
}

在 log4j2 的 PluginRegistry.java 中含有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static volatile PluginRegistry INSTANCE;
private static final Object INSTANCE_LOCK = new Object();

/**
* Returns the global PluginRegistry instance.
*
* @return the global PluginRegistry instance.
* @since 2.1
*/
public static PluginRegistry getInstance() {
PluginRegistry result = INSTANCE;
if (result == null) {
synchronized (INSTANCE_LOCK) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = new PluginRegistry();
}
}
}
return result;
}

以上两个开源项目的实现中都使用的带本地变量的双重检查加锁实现。

该问题在 《Effective Java中文版(第3版)》 第 83 条:慎用延迟初始化 中也有描述,可以参考。

Reference

Java Concurrency (&c): Double Checked Locking
Double-checked locking - Wikipedia
volatile (computer programming) - Wikipedia