为什么在HashMap.keySet()中声明局部变量ks?

时间:2017-09-04 09:57:04

标签: java

我查看了源代码java.util.HashMap并看到了以下代码:

public Set<K> keySet() {
    Set<K> ks;
    return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}

(Windows,java版&#34; 1.8.0_111&#34;)

在我的MacBook上,它看起来像这样:

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

(MacOs X Sierra,java版&#34; 1.8.0_121&#34;)

为什么两个变体都声明了局部变量ks?为什么它不是这样写的:

public Set<K> keySet() {
    if (keySet == null) {
        keySet = new KeySet();
    }
    return keySet;
}

public Set<K> keySet() {
    return keySet == null ? (keySet = new KeySet()) : keySet;
}

2 个答案:

答案 0 :(得分:12)

JavaDoc有答案:

/**
 * Since there is no synchronization performed while accessing these fields,
 * it is expected that java.util.Map view classes using these fields have
 * no non-final fields (or any fields at all except for outer-this). Adhering
 * to this rule would make the races on these fields benign.
 *
 * It is also imperative that implementations read the field only once,
 * as in:
 *
 * public Set<K> keySet() {
 *   Set<K> ks = keySet;  // single racy read
 *   if (ks == null) {
 *     ks = new KeySet();
 *     keySet = ks;
 *   }
 *   return ks;
 * }
 *}
 */
transient Set<K> keySet;

答案 1 :(得分:4)

据我所知,这是一个非常整洁的优化。

这是以前写的:

if (keySet == null) { // volatile read
         keySet = new AbstractSet<K>() { // volatile write
          ....

return keySet; // volatile read

由于此处插入了内存屏障,因此无法重新排序这些操作。所以它看起来像这样:

 [StoreLoad]
 // volatile read
 [LoadLoad]
 [LoadStore]

 [StoreStore]
 [LoadStore]
 // volatile write
 [StoreLoad]

 [StoreLoad] // there's probably just one barrier here instead of two
 // volatile read
 [LoadLoad]
 [LoadStore]

此处存在许多障碍,最昂贵的是StoreLoad上发出的x86

假设我们放弃了volatile。由于没有插入障碍,这些操作可以以任何方式重新排序,这里有keySet变量的两个有效读取。

我们可以有一个racy读取并将变量存储到本地字段中(因为它们是本地的,它们是线程安全的 - 没有人可以更改本地声明的引用),这是我能看到的唯一问题是多个线程可能同时看到一个空引用并用空KeySet初始化它并可能做太多工作;但这很可能比障碍便宜。

另一方面,如果某些线程看到非空引用,它将100%看到完全初始化的对象,这是关于final字段的注释。如果所有对象都是最终的,则JMM保证构造函数之后的“冻结”操作;或者用更简单的单词(IMO)如果所有字段都是最终字段并在构造函数中初始化,则在其后插入两个障碍:LoadStoreLoadLoad;从而达到同样的效果。