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

时间:2012-01-24 06:28:11

标签: java thread-safety lazy-loading

当我阅读维基百科有关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存储在堆中?

2 个答案:

答案 0 :(得分:2)

由于Java内存模型允许的读取重新排序,简单地使用helperWrapper进行空检查和返回语句可能会失败。

以下是示例方案:

  1. 第一个helperWrapper == null(racy read)测试评估为false,即helperWrapper不为null。
  2. 最后一行return helperWrapper.value(racy read)导致NullPointerException,即helperWrapper为null
  3. 这是怎么发生的? 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,逐步解释了这个问题:

  

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

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

注意上面提到的某些编程语言的语义正是Java 1.5及更高版本的语义。 Java内存模型(JSR-133)明确允许这样的行为 - 如果您感兴趣,可以在网上搜索更多详细信息。

  

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

不,上面不是原因。

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