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

And*_*suk 9 java

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

public Set<K> keySet() {
    Set<K> ks;
    return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
Run Code Online (Sandbox Code Playgroud)

(Windows,java版"1.8.0_111")

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

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}
Run Code Online (Sandbox Code Playgroud)

(MacOs X Sierra,java版"1.8.0_121")

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

public Set<K> keySet() {
    if (keySet == null) {
        keySet = new KeySet();
    }
    return keySet;
}
Run Code Online (Sandbox Code Playgroud)

要么

public Set<K> keySet() {
    return keySet == null ? (keySet = new KeySet()) : keySet;
}
Run Code Online (Sandbox Code Playgroud)

Car*_*geh 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;
Run Code Online (Sandbox Code Playgroud)


Eug*_*ene 4

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

之前是这样写的:

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

return keySet; // volatile read
Run Code Online (Sandbox Code Playgroud)

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

 [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]
Run Code Online (Sandbox Code Playgroud)

这里有很多障碍,最昂贵的是StoreLoad在 上发出的障碍x86

假设我们把volatile这里放下。由于没有插入障碍,这些操作可以以任何他们喜欢的方式重新排序,并且这里有两个对变量的活泼读取keySet

我们可以有一个简单的读取并将变量存储到本地字段中(因为它们是本地的,所以它们是线程安全的 - 没有人可以更改本地声明的引用),据我所知,唯一的问题是多个线程可能会同时看到空引用并用空值对其进行初始化 KeySet,并且可能会执行过多的工作;但这很可能比障碍更便宜。

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