为什么这个双重检查锁使用单独的包装类实现?

Aud*_*rey 10 java lazy-loading thread-safety

当我阅读维基百科的关于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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我根本不明白为什么我们需要创建包装器.这不够吗?

if (helperWrapper == null) {
    synchronized(this) {
        if (helperWrapper == null) {
            helperWrapper = new FinalWrapper<Helper>(new Helper());
        }
    }
}    
Run Code Online (Sandbox Code Playgroud)

是因为使用包装器可以加速初始化,因为包装器存储在堆栈上并且helperWrapper存储在堆中?

gna*_*nat 1

这还不够吗?

if (helperWrapper == null) {
     synchronized(this) {
       if (helperWrapper == null) {
          helperWrapper = new FinalWrapper<Helper>(new Helper());
       }
     }
}
Run Code Online (Sandbox Code Playgroud)

不,这还不够。

上面,首先检查是否helperWrapper == null不是线程安全的。对于某些线程“太早”,它可能返回 false(看到非空实例),指向未完全构造的 helperWrapper 对象。

您引用的维基百科文章逐步解释了这个问题:

例如,考虑以下事件序列:

  1. 线程A注意到该值没有初始化,因此它获得锁并开始初始化该值。
  2. 由于某些编程语言的语义,允许编译器生成的代码在 A 完成初始化之前更新共享变量以指向部分构造的对象。
  3. 线程 B 注意到共享变量已被初始化(或者看起来如此),并返回其值。因为线程 B 认为该值已经初始化,所以它不会获取锁。如果 B 在 A 完成的所有初始化被 B 看到之前使用该对象(因为 A 尚未完成初始化,或者因为对象中的某些初始化值尚未渗透到 B 使用的内存(缓存一致性)) ,程序可能会崩溃。

注意,上面提到的一些编程语言的语义与 Java 1.5 及更高版本的语义完全相同。Java 内存模型 (JSR-133) 明确允许此类行为 - 如果您感兴趣,请在网络上搜索更多详细信息。

是不是因为使用wrapper可以加快初始化速度,因为wrapper存储在栈上,而helperWrapper存储在堆上?

不,上面不是原因。

原因是线程安全。同样,Java 1.5 及更高版本的语义(如 Java 内存模型中定义)保证任何线程都只能从包装器访问正确初始化的 Helper 实例,因为它是在构造函数中初始化的最终字段 - 请参阅JLS 17.5 Final字段语义

  • 我不确定,但我认为这没有抓住问题的重点。正如您所说,如果“helperWrapper”可以引用部分构造的 FinalWrapper 对象,那么将引用复制到局部变量“wrapper”究竟有何帮助?现在“wrapper”指的是部分构造的对象,它也有同样的问题。 (2认同)