活泼的单检查习惯用法是一种无需同步或
volatile
的延迟初始化技术。当初始化允许多个线程并发执行时可以使用它,但只要结果一致即可。 JDK 本身就是这样做的,例如在 ConcurrentHashMap.keySet
(JDK 17):
public KeySetView<K,V> keySet() {
KeySetView<K,V> ks;
if ((ks = keySet) != null) return ks;
return keySet = new KeySetView<K,V>(this, null);
}
此技术仅适用于原始值或不可变类,其中每个字段都必须是最终的以确保安全发布。 这里有一篇德语文章,深入解释了它。
现在我们讨论了当初始化期间有相同类型的中间结果时使用此技术是否安全。例如:
class SumUpToTen {
private Integer result;
int getResult() {
Integer res = result;
if (res != null)
return res;
int sum = 0;
for (int i = 1; i <= 10; i++)
sum += i;
result = sum;
return sum;
}
}
在这里,我们使用简洁的单项检查惯用法来初始化从 1 到 10 的所有整数之和的值。这当然是一件愚蠢的事情,但这是一个例子,在初始化情况下,我们有几个与最终结果类型相同的中间结果。
现在我们知道,在缺乏同步和
volatile
的情况下,只要单线程行为保持不变,编译器就可以对指令重新排序。
问题是: 是否可以将中间结果分配给实例字段,即用对字段
sum
的访问替换局部变量 result
?
一方面,如果这样做的话,单线程行为确实会保持不变。
另一方面,为什么要这么做呢?为字段赋值比为局部变量赋值要慢,因此分配中间结果没有任何好处。其次,这不仅仅是指令的重新排序,而是将对局部变量的访问替换为对实例字段的访问。另外,如果该模式的安全性取决于如此小的细节,那么我认为该模式会被认为太脆弱,没有人会使用它。
因此,我认为编译器不能分配中间结果,并且我确实认为在这种情况下该模式是安全的,但我想仔细检查。
另外,我通常会在自己的私有方法中提取值的计算,例如
computeResult
,但我在示例中没有这样做,以更好地显示“潜在的不安全性”。我非常有信心,无论你是否提取它,都不会影响该模式的安全性,但你也能确认这一点吗?
问题是: 是否可以将中间结果分配给实例字段,即用对字段
的访问替换局部变量
sum
?
result
只要程序执行的结果与该程序的java代码的某些合法(根据java语言语义)执行的结果相同,JVM就可以在内部执行任何操作。
所以答案是“视情况而定”: