1 | package me.tianshuang; |
以上的双重检查实现并不安全,原因为编译器可以重排序代码以使 Helper()
构造函数中的代码在写入 helper
变量后执行。如果发生了这样的情况,那么在构造线程写入 helper
变量之后,但在它实际完成对象构造之前,另一个线程可以在完成初始化之前出现并读取 helper
,此时,该线程可能会看到对 helper
对象的非空引用,但会看到 helper
对象字段的默认值,而不是构造函数中设置的值。
什么意思呢,加入 Helper
类的源码如下:
1 | package me.tianshuang; |
编译器可能将 this.a = 1
重排序至 Helper
类实例赋值给 helper
之后执行,导致其他线程看到的 helper
中的变量 a
的值为 0,即其他线程看到了未完全构造的对象。在 The “Double-Checked Locking is Broken” Declaration 中使用了 Symantec JIT 系统对这种场景进行了举例,如下代码 singletons[i].reference = new Singleton();
编译后生成的汇编代码如下:
1 | 0206106A mov eax,0F97E78h |
可以看出,申请对象空间后,先将对象地址赋值给 singletons[i].reference
,再执行的构造函数中的初始化代码,这将导致其他线程看到未完全构造的对象。
在 JDK 5.0 及更高版本中扩展了 volatile
的语义,禁止了对 volatile
字段的指令重排序,通过将 helper
变量声明为 volatile
可以解决上面的问题,
1 | package me.tianshuang; |
一个更高效的写法为:
1 | package me.tianshuang; |
本地变量 localRef
的引入能将方法的性能提升 40%,因为大多数时候,在 helper
变量已经初始化的情况下,localRef
的引入能够使 volatile
变量仅被访问一次。
比如在 Guava 的 RateLimiter.java 中含有如下代码:
1 | // Can't be initialized in the constructor because mocks don't call the constructor. |
在 log4j2 的 PluginRegistry.java 中含有如下代码:
1 | private static volatile PluginRegistry INSTANCE; |
以上两个开源项目的实现中都使用的带本地变量的双重检查加锁实现。
该问题在 《Effective Java中文版(第3版)》 第 83 条:慎用延迟初始化 中也有描述,可以参考。
Reference
Java Concurrency (&c): Double Checked Locking
Double-checked locking - Wikipedia
volatile (computer programming) - Wikipedia