我正在努力完成有效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;
}
所以,我实际上有几个问题:
为什么field
上的volatile修饰符需要初始化发生在同步块中?本书提供了这个支持文本:“因为如果字段已经初始化,则没有锁定, critical 该字段被声明为volatile”。因此,在初始化字段的情况下,如果缺少其他同步,volatile是field
上多个线程一致视图的唯一保证吗?如果是这样,为什么不同步getField()或上述代码提供更好的性能?
该文本表明,不需要的局部变量result
用于“确保field
在已经初始化的常见情况下只读取一次”,从而改进性能。如果删除了result
,那么在已经初始化field
的常见情况下,如何多次阅读{{1}}?
答案 0 :(得分:16)
为什么在字段上需要volatile修饰符,因为初始化发生在同步块中?
volatile
是必要的,因为可能需要重新排序对象构造周围的指令。 Java内存模型声明实时编译器可以选择重新排序指令,以便在对象构造函数之外移动字段初始化。
这意味着thread-1可以初始化field
内的synchronized
,但该线程2可能会看到该对象未完全初始化。在将对象分配给field
之前,不必初始化任何非最终字段。 volatile
关键字可确保field
在访问之前已完全初始化。
这是着名的"double check locking" bug。
的一个例子如果删除了结果,在已经初始化的常见情况下,如何多次读取字段?
只要您访问volatile
字段,就会导致内存屏障被越过。与访问普通字段相比,这可能是昂贵的。将volatile
字段复制到局部变量是一种常见模式,如果要在同一方法中以任何方式多次访问它。
有关线程之间没有内存屏障的共享对象的危险的更多示例,请参阅我的答案:
About reference to object before object's constructor is finished
答案 1 :(得分:7)
这是一个相当复杂但现在与编译器可以重新排列的事情有关
基本上Double Checked Locking
模式在Java中不起作用,除非变量是volatile
。
这是因为,在某些情况下,编译器可以将变量分配给null以外的其他内容,然后对变量进行初始化并重新分配。另一个线程会看到变量不为null并尝试读取它 - 这可能会导致各种非常特殊的结果。
查看关于该主题的this其他SO问题。
答案 2 :(得分: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修复的?在发生易失性写入之后,在易失性写入之前发生的所有写入都是可见的。
如果删除了结果,在已经初始化的常见情况下,如何多次读取字段?
在本地存储一次并在整个方法中读取可确保一个线程/进程本地存储和所有线程本地读取。你可以争论那些方面的过早优化,但我喜欢这种风格,因为如果你不这样做,你就不会遇到奇怪的重新排序问题。