'Effective Java'难题:为什么这个并发代码需要volatile?

Chr*_*ght 15 java concurrency volatile

我正在努力完成有效Java(第二版)的第71项"明智地使用懒惰初始化".它建议使用双重检查习惯用于使用此代码(pg 283)实例字段的延迟初始化:

private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) {  //First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null)  //Second check (with locking)
                 field = result = computeFieldValue();
        }
     }
     return result;
}
Run Code Online (Sandbox Code Playgroud)

所以,我实际上有几个问题:

  1. 为什么field在同步块中进行初始化时需要volatile修饰符?本书提供了这样的支持文本:"因为如果字段已经初始化,则没有锁定,因此将字段声明为volatile 是至关重要的".因此,是否一旦字段初始化,volatile是多线程一致视图的唯一保证,field因为缺少其他同步?如果是这样,为什么不同步getField()或上述代码提供更好的性能?

  2. 该文本表明,不需要的局部变量result用于"确保field在已经初始化的常见情况下只读取一次",从而提高性能.如果result被删除,field在已经初始化的常见情况下将如何多次读取?

Gra*_*ray 16

为什么在字段上需要volatile修饰符,因为初始化发生在同步块中?

volatile是必要的,因为围绕对象构造的指令可能重新排序.Java内存模型声明实时编译器可以选择重新排序指令以在对象构造函数之外移动字段初始化.

这意味着thread-1可以初始化a的field内部,synchronized但是线程2可能会看到未完全初始化的对象.在将对象分配给之前,不必初始化任何非最终字段field.该volatile关键字可确保field被访问之前,它作为完全初始化.

这是着名的"双重检查锁定"错误的一个例子.

如果删除了结果,那么在已经初始化的常见情况下,如何多次读取字段?

无论何时访问某个volatile字段,都会导致跨越内存屏障.与访问普通字段相比,这可能是昂贵的.将volatile字段复制到局部变量是一种常见的模式,如果要在同一方法中以任何方式多次访问它.

有关线程之间没有内存屏障的共享对象的危险的更多示例,请参阅我的答案:

关于在对象的构造函数完成之前对对象的引用

  • 换句话说,正如John Wint在另一个答案中解释的那样,没有保证可见性意味着在完整构造变得可见之前,您的新对象的引用可能对您的线程可见(即它将`field`视为非null) (即`field.member`可能读为null,即使它在构造函数中初始化). (2认同)
  • @ Medo42这是关于[同步顺序](http://docs.oracle.com/javase/specs/jls/se5.0/html/memory.html#17.4.4),而不是可见性.对于赋值的*立即可见性*,此问题仍然存在.因此,添加了before-before*ordering*,将其从破坏的DCL更改为工作DCL. (2认同)

Bor*_*der 7

这相当复杂,但它与现在编译器可以重新排列的东西有关.除非变量是,否则
基本上该Double Checked Locking模式在Java中不起作用volatile.

这是因为,在某些情况下,编译器可以将变量赋值为null以外的值,然后对变量进行初始化并重新分配.另一个线程会看到变量不为null并尝试读取它 - 这可能会导致各种非常特殊的结果.

看看对主题的其他SO问题.

  • 谢谢@ bmorris591.从现在开始,我将把我所有的错误称为"特殊结果":) (2认同)