有谁可以解释为什么这个例子在没有volatile的情况下是线程安全的?
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
实际上,假设computeHashCode函数总是返回相同的结果且没有副作用(即幂等),您甚至可以摆脱所有同步。
// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo {
private int cachedHashCode = 0;
public int hashCode() {
int h = cachedHashCode;
if (h == 0) {
h = computeHashCode();
cachedHashCode = h;
}
return h;
}
// other functions and members...
}
更多:我明白了,我们不关心值是否计算两次(因此它不是真正的线程安全)。我还想知道在计算完哈希码之后创建的新线程是否可以保证看到新的哈希码?
答案 0 :(得分:7)
这是在冰上行走,但这是解释。可见性问题意味着某些线程可能会看到旧版本和一些新版本。在我们的例子中,一些线程看到0
而其他线程看到cachedHashCode
。
调用hashCode()
并看到cachedHashCode
的帖子只会返回它(if (h == 0)
条件不符合)并且一切正常。
但是看到0
的线程(尽管cachedHashCode
可能已经计算过)可能会再次重新计算它。
换句话说,在最糟糕的情况下,每个线程都会第一次进入看0
的分支(就像是ThreadLocal
一样)。
由于computeHashCode()
是幂等的(非常重要),因此多次(通过不同的线程)调用它并再次将其重新分配给同一个变量不应该有任何副作用。
答案 1 :(得分:6)
这里重要的信息是
computeHashCode函数总是返回相同的结果
如果这是真的,那么已知computeHashCode实际上是不可变的,因为它总是相同的值,你永远不会有并发问题。
至于使cachedHashCode易变。就线程安全而言,它不会有所作为,因为你总是分配并返回一个线程局部变量h
,它将是一个非零的computedHashCode。
答案 2 :(得分:4)
这被称为生动的单一检查成语。它在计算一个值是幂等的时候使用(每次都返回相同的值;推论:类型必须是不可变的)并且便宜(如果它被意外地重新计算不止一次那就没关系)。这总是采取某种形式
class Foo {
private Value cacheField; // field holding the cached value; not volatile!
public Value getValue() {
Value value = cacheField; // very important!
if (value == 0 or null or whatever) {
value = computeValue();
cacheField = value;
}
return value;
}
}
或多或少等同的东西。如果你的实现不是幂等的,或者不便宜,那么你应该使用另一个成语;有关详细信息,请参阅Effective Java第71项。但重点是,线程最多只读取一个cacheField
,如果它们在未计算值的状态下看到cacheField
,则会重新计算该值。
如Effective Java中所述,这就是String.hashCode()
的实现方式,例如。
答案 3 :(得分:0)
如果Foo对于有助于哈希码的字段是不可变的,那只会是真的。 (必须满足“假设computeHashCode函数总是返回相同的结果”)
否则我不同意它是线程安全的。
线程1可能会或者可能无法在哈希映射中找到密钥,具体取决于jvm如何选择处理该cachedHashCode。如果喜欢,jvm可以选择保留非易失性字段的单独副本。 Volatile仅确保jvm不会这样做并且所有线程始终看到相同的值。