【问题标题】:Why does ConcurrentHashMap use a local variable `tab` to reference the table?为什么 ConcurrentHashMap 使用局部变量 `tab` 来引用表?
【发布时间】:2020-04-15 09:28:59
【问题描述】:

ConcurrentHashMap.putVal()(JDK 版本:11;ConcurrentHashMap.java;第 1010 行)

final V putVal(K key, V value, boolean onlyIfAbsent) {
   if (key == null || value == null) throw new NullPointerException();
   int hash = spread(key.hashCode());
   int binCount = 0;
   for (Node<K,V>[] tab = table;;) {
       ...
   }
   addCount(1L, binCount);
   return null;
}

为什么它使用变量tab 来引用表? 同样在ConcurrentHashMap.get()(从第 934 行开始)

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

【问题讨论】:

  • 这是在 ConcurrentHashMap 的源代码中还是在其他人提供的示例代码中?
  • 这是从哪里来的?请链接到源代码。
  • @NomadMaker 是的,这是在源代码 int JDK (version 11) ConcurrnentHashMap 1014 行
  • @Thomas Kläger 在堆栈帧中使用新引用可以更快地获取表对象?
  • 链接到源代码会方便读者。

标签: java concurrenthashmap


【解决方案1】:

如果您使用table,它指向的实例可能会在处理它时发生变化,这可能会导致未定义的行为或异常。因此,您需要在本地“固定”它并使用该局部变量。

假设这样做是为了防止未定义的行为,如果它不应该被两个线程在写入模式下同时使用*。 table指向的实例即使在非并发HashMap中也可以改变。

替代方法是使用关键字 synchronized,但这会降低性能。

* 如果在多个线程持有它时未对其进行操作,您可以在多个线程中从 HashMap 中读取而不会出现问题。

【讨论】:

  • 感谢您的分析。你能举个例子来更详细地说明吗?
【解决方案2】:

resize() 方法设置table = newTabHashMap 中更容易理解为什么Java 会这样做。在resize() 操作期间读取table 的任何方法都会从它们下面拉出引用并重新分配,这将导致不可预知的行为。

Volatile 可以确保读取方法更新为最新的table;但这根本不是我们想要的。我们希望读取方法继续使用它开始读取时 table 中的值不间断。

Synchronized 可以阻止读取和写入同时发生,但会降低性能。如果需要,我们可以恢复使用Hashtable

同样的基本推理适用于ConcurrentHashMap 及其更复杂的transfer() 方法,该方法也重新分配table 引用。引用被复制到一个局部变量中,以避免在重新分配期间丢失它。

【讨论】:

  • HashTable 也使用这个
  • java public synchronized V get(Object key) { Entry&lt;?,?&gt; tab[] = table; int hash = key.hashCode(); int index = (hash &amp; 0x7FFFFFFF) % tab.length; for (Entry&lt;?,?&gt; e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) &amp;&amp; e.key.equals(key)) { return (V)e.value; } } return null; }
  • Hashtable 中的 rehash() 方法实际上是同步的,因此显然它仍然必须防止相同的 table 重新分配。
【解决方案3】:

对于性能而言,在方法中使用局部变量比使用全局变量更有效。 让我们看看代码块。

public class LocalFieldDemo {
  private String[] globalArr = new String[123];
  public void test() {
      String[] localArr = globalArr;
      for (int i = 0; i < 123; i++) {
          System.out.println(localArr[i]);
      }
  }
}

我们看到了字节码

public void test();
  Code:
     0: aload_0
     1: getfield      #3                  // Field globalArr:[Ljava/lang/String;
     4: astore_1
     5: iconst_0
     6: istore_2
     7: iload_2
     8: bipush        123
    10: if_icmpge     28
    13: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
    16: aload_1
    17: iload_2
    18: aaload
    19: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    22: iinc          2, 1
    25: goto          7
    28: return

并查看第二个代码块:

public class InstanceFieldDemo {
  private String[] globalArr = new String[123];
  public void test() {
      for (int i = 0; i < 123; i++) {
          System.out.println(globalArr[i]);
      }
  }
}

这个代码块的字节码

public void test();
  Code:
     0: iconst_0
     1: istore_1
     2: iload_1
     3: bipush        123
     5: if_icmpge     26
     8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
    11: aload_0
    12: getfield      #3                  // Field globalArr:[Ljava/lang/String;
    15: iload_1
    16: aaload
    17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    20: iinc          1, 1
    23: goto          2
    26: return

有什么不同呢? 我们可以看到,在第二个字节码中

11: aload_0
12: getfield      #3                  // Field globalArr:[Ljava/lang/String;

但第一个字节码:

16: aload_1

如果我们通过全局变量获取值,jvm必须通过命令aload_0获取this,并获取字段globalArr。在第二个代码中,我们只使用了一个命令aload 来获取值。第二个代码块性能更好。

【讨论】:

    猜你喜欢
    • 2016-08-19
    • 2013-07-05
    • 2014-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-08
    相关资源
    最近更新 更多