一尘不染

如何解决Java中的“双重检查锁定已损坏”声明?

java

我想为Java中的多线程实现延迟初始化。
我有一些这样的代码:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            Helper h;
            synchronized(this) {
                h = helper;
                if (h == null) 
                    synchronized (this) {
                        h = new Helper();
                    } // release inner synchronization lock
                helper = h;
            } 
        }    
        return helper;
    }
    // other functions and members...
}

我得到了“双重检查锁定已损坏”声明。
我该如何解决?


阅读 400

收藏
2020-03-22

共1个答案

一尘不染

以下是第71项中建议的惯用语:明智地使用 Effective Java:

如果你需要使用延迟初始化来提高实例字段的性能,请使用double-check idiom。这种习惯用法避免了在初始化字段后访问字段时发生锁定的费用(项67)。习惯用法的想法是检查字段的值两次(因此,将其命名为double-check):一次不锁定,然后,如果该字段似乎未初始化,则第二次锁定。仅当第二次检查表明该字段未初始化时,该调用才会初始化该字段。因为如果字段已经初始化就没有锁定,所以声明该字段至关重要volatile(项目66)。这是成语:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result != null) // First check (no locking)
        return result;
    synchronized(this) {
        if (field == null) // Second check (with locking)
            field = computeFieldValue();
        return field;
    }
}

此代码可能看起来有些混乱。特别是,对于局部变量结果的需求可能不清楚。此变量的作用是确保在已初始化字段的常见情况下,该字段仅被读取一次。尽管不是绝对必要的,但是这可以提高性能,并且通过应用于低级并发编程的标准可以更加优雅。在我的机器上,上述方法比不带局部变量的明显方法快25%。

在1.5版之前,由于volatile修饰符的语义不足以支持它,所以双重检查惯用语不能可靠地工作[Pugh01]。版本1.5中引入的内存模型解决了此问题[JLS,17,Goetz06 16]。今天,仔细检查惯用语是延迟初始化实例字段的一种选择技术。虽然你也可以将双重检查惯用法应用于静态字段,但没有理由这样做:惰性初始化持有人类惯用法是更好的选择。

参考
- 有效的Java,第二版
- 项目71:明智地使用延迟初始化

2020-03-22