带有中间结果的活泼的单项检查习语

问题描述 投票:0回答:1

活泼的单检查习惯用法是一种无需同步或

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
,但我在示例中没有这样做,以更好地显示“潜在的不安全性”。我非常有信心,无论你是否提取它,都不会影响该模式的安全性,但你也能确认这一点吗?

java multithreading concurrency lazy-initialization
1个回答
0
投票

问题是: 是否可以将中间结果分配给实例字段,即用对字段

sum
的访问替换局部变量
result

只要程序执行的结果与该程序的java代码的某些合法(根据java语言语义)执行的结果相同,JVM就可以在内部执行任何操作。

所以答案是“视情况而定”:

  • 如果另一个线程看到分配的中间结果(即,它与第一个线程并行读取实例字段,并且读取返回这些中间值),那么这将是非法优化,因为它向实例字段引入了额外的写入,而这些写入是非法的。 t出现在程序代码中
  • 但是如果没有其他线程看到这一点,那么这是允许的
© www.soinside.com 2019 - 2024. All rights reserved.