2012-02-01 48 views
4

任何人都可以解释为什么这个例子是线程安全的没有易失性?线程安全无易失性

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

也许你会考虑如果不使用synchronized或volatile会发生什么会更有用,你会明白为什么不需要它。 – 2012-02-01 22:02:32

+0

请注意,“Foo”实际上是不可变的,这一点很重要。这保证'hashCode()'是幂等的。 – Kevin 2016-02-17 19:28:48

回答

6

这是在薄冰上行走,但这里是解释。可见性问题意味着一些线程可能会看到旧版本和一些新的版本。在我们的案例中,一些线程看到0而其他线程 - cachedHashCode

线程调用hashCode()cachedHashCode将返回它(if (h == 0)条件不符合),并且一切正常。

但看到0(尽管cachedHashCode可能已被计算)的线程将重新计算它。

换句话说,在最坏的情况下,每个线程首次会看到0(如果它是ThreadLocal),将进入分支。

由于computeHashCode()是幂等(非常重要),它们都会多次调用(通过不同的线程)并将其重新分配给同一个变量应该没有任何副作用。

6

信息的重要棋子这里是

computeHashCode函数总是返回相同的结果

如果为真,那么computeHashCode被称为是有效不可变的,因为它永远是相同的价值,你永远不会有并发问题。

至于使cachedHashCode易变。就线程安全而言,它不会有所作为,因为您总是分配并返回一个线程局部变量h,它将是一个非零的computedHashCode。

+4

另一个关键点是类变量是一个int,内存模型保证它是原子更新的。因此,尽管一个线程可能会看到“stale”0值并重新计算哈希码,但它永远不会看到部分初始化的int(比如说低16位)。长期以来,情况并非如此。 – 2012-02-01 21:43:46

+0

的确如此。如果cachedHashCode是一个long/double,你肯定会失去int的原子性。 – 2012-02-01 21:47:46

0

这只有在Foo对于有助于哈希码的字段不可变的情况下才会如此。 (必须满足“假设computeHashCode函数总是返回相同的结果”)

否则我不同意它会是线程安全的。

  • 线程1进入Hashcode(),获取半途,并在返回5(例如)之前暂停。
  • 线程2输入computeHashCode()并在其中途暂停
  • 线程3以影响哈希码的方式修改对象。
  • 线程1重新启动,将散列码设置为5并将该对象放置在散列表中作为键。
  • 线程2个重新启动后,设置哈希码78392.

线程1可能会或可能不会以后能够找到散列图的关键,取决于JVM是如何选择来处理cachedHashCode。如果喜欢,jvm可以选择保留非易失性字段的单独副本。易失性仅确保jvm不会这样做,并且所有线程始终都能看到相同的值。

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; 
    } 
} 

或更多或更少的等价物。如果你的实现不是幂等的,或者不便宜,那么你应该使用另一个习惯用法;详细信息请参阅有效的Java项目71。但重点是,线程最多可以读取到cacheField,如果他们看到cacheField处于未计算值的状态,则重新计算该值。

正如Effective Java所述,例如,String.hashCode()就是如何实现的。