当我阅读维基百科有关Double Checked Locking成语的文章时,我对其实施感到困惑:
public class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}
public class Foo {
private FinalWrapper<Helper> helperWrapper = null;
public Helper getHelper() {
FinalWrapper<Helper> wrapper = helperWrapper;
if (wrapper == null) {
synchronized(this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<Helper>(new Helper());
}
wrapper = helperWrapper;
}
}
return wrapper.value;
}
}
我根本不明白为什么我们需要创建包装器。这还不够吗?
if (helperWrapper == null) {
synchronized(this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<Helper>(new Helper());
}
}
}
是否因为使用包装器可以加速初始化,因为包装器存储在堆栈中并且helperWrapper存储在堆中?
答案 0 :(得分:2)
由于Java内存模型允许的读取重新排序,简单地使用helperWrapper进行空检查和返回语句可能会失败。
以下是示例方案:
helperWrapper == null
(racy read)测试评估为false,即helperWrapper不为null。return helperWrapper.value
(racy read)导致NullPointerException,即helperWrapper为null 这是怎么发生的? Java内存模型允许对这两个有效读取进行重新排序,因为在读取之前没有屏障,即没有“之前发生”关系。 (见String.hashCode example)
请注意,在您阅读helperWrapper.value
之前,您必须隐式阅读helperWrapper
引用本身。因此,完全实例化helperWrapper的final
语义提供的保证不适用,因为当helperWrapper不为null时,仅适用。
答案 1 :(得分:0)
这还不够吗?
if (helperWrapper == null) { synchronized(this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper<Helper>(new Helper()); } } }
这还不够。
上面,首先检查helperWrapper == null
是不是线程安全的。它可能会为某个线程“太早”返回false(看到非null实例),指向未完全构造的helperWrapper对象。
您提到的Wikipedia article,逐步解释了这个问题:
例如,请考虑以下事件序列:
- 线程A注意到该值未初始化,因此它获取锁定并开始初始化该值。
- 由于某些编程语言的语义,允许编译器生成的代码更新共享变量 在A完成之前指向部分构造的对象 执行初始化。
- 线程B注意到共享变量已初始化(或显示),并返回其值。因为线程B相信 值已经初始化,它不会获得锁定。如果B使用 在B看到A完成所有初始化之前的对象 (要么因为A还没有完成初始化,要么是因为某些原因 对象中的初始化值尚未渗透到 内存B使用(缓存一致性)),程序可能会崩溃。
醇>
注意上面提到的某些编程语言的语义正是Java 1.5及更高版本的语义。 Java内存模型(JSR-133)明确允许这样的行为 - 如果您感兴趣,可以在网上搜索更多详细信息。
是否因为使用包装器可以加速初始化,因为包装器存储在堆栈中并且helperWrapper存储在堆中?
不,上面不是原因。
原因是线程安全。同样,Java 1.5及更高版本的语义(如Java内存模型中所定义)保证任何线程都能够仅从包装器访问正确初始化的Helper实例,因为它是在构造函数中初始化的最终字段 - 请参阅{{3 }}