15

我正在研究 Effective Java(第二版)的第 71 项“明智地使用延迟初始化”。它建议使用以下代码(第 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;
}

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

  1. field鉴于初始化发生在同步块中,为什么需要 volatile 修饰符?这本书提供了以下支持文本:“因为如果字段已经初始化,则没有锁定,因此将字段声明为 volatile至关重要”。因此,是不是一旦字段被初始化,field在没有其他同步的情况下,volatile 是多线程一致视图的唯一保证?如果是这样,为什么不同步 getField() 或者上面的代码提供了更好的性能?

  2. 文本建议使用不需要的局部变量 ,result以“确保field在已初始化的常见情况下仅读取一次”,从而提高性能。如果result被删除,在已经初始化的常见情况下如何field多次读取?

4

3 回答 3

16

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

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

这意味着线程 1 可以初始化fielda 的内部,synchronized但线程 2 可能会看到对象未完全初始化。在将对象分配给field. 该volatile关键字确保field在访问之前已完全初始化。

这是著名的“双重检查锁定”错误的一个例子。

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

每当您访问一个volatile字段时,都会导致跨越内存障碍。与访问普通字段相比,这可能会很昂贵。volatile如果要在同一方法中以任何方式多次访问字段,则将字段复制到局部变量中是一种常见模式。

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

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

于 2013-02-17T23:07:04.997 回答
7

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

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

看看这个关于这个话题的另一个 SO question。

于 2013-02-17T23:09:44.897 回答
3

好问题。

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

如果您没有同步,并且您分配给该共享全局字段,则无法保证在构造该对象时发生的所有写入都会被看到。例如想象 FieldType 的样子。

public class FieldType{
   Object obj = new Object();
   Object obj2 = new Object();
   public Object getObject(){return obj;}
   public Object getObject2(){return obj2;}
}

有可能getField()返回一个非空实例,但该实例getObj()getObj2()方法可以返回空值。这是因为如果没有同步,对这些字段的写入可能会与对象的构造竞争。

这是如何用 volatile 解决的?在易失性写入发生之后,发生在易失性写入之前的所有写入都是可见的。

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

本地存储一次并在整个方法中读取可确保一个线程/进程本地存储和所有线程本地读取。你可以在这些方面争论过早的优化,但我喜欢这种风格,因为如果你不这样做,你就不会遇到奇怪的重新排序问题。

于 2013-02-17T23:27:25.010 回答