【问题标题】:Java 8 ConcurrentHashMapJava 8 并发哈希映射
【发布时间】:2017-06-09 14:35:37
【问题描述】:

我观察到 ConcurrentHashMap 已在 Java 8 中完全重写,使其更加“无锁”。我浏览了get()方法的代码,发现没有明确的锁定机制:

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

问题:

如何从一个线程查看其他线程对此哈希图所做的修改,因为代码不在同步保护伞下(这将强制执行 happens-before关系)?

注意:整个 ConcurrentHashMap 是一个表的包装器: transient volatile Node<K,V>[] table;

所以table 是对数组的 volatile 引用,而不是对 volatile 元素数组的引用! 这意味着如果有人更新此数组中的元素,则修改不会在其他线程中可以看到。

【问题讨论】:

  • 您可能会发现这是一个(附加的)好读物:javaspecialists.eu/archive/Issue235.html
  • ConcurrentHashMap 实际上使用Unsafe 从数组元素中执行易失性读取。这就是tabAt 方法的作用。
  • 澄清一下,@Radiodef Unsafe 的同步在 puts 上没有读取。

标签: java concurrency


【解决方案1】:

简答

Node#valvolatile,它确定您在订购之前发生的事情。

更长的答案

synchronized 不是线程安全的要求,它是工具箱中的一个工具,可以使系统线程安全。您必须考虑对此 ConcurrentHashMap 的一整套操作来推理线程安全性。

知道原来的ConcurrentHashMap 也是非阻塞的很有用。注意 Java 8 之前的 CHM 获取

V get(Object key, int hash) {
    if (count != 0) { // read-volatile
        HashEntry<K,V> e = getFirst(hash);
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value;
                if (v != null)
                    return v;
                return readValueUnderLock(e); // ignore this
            }
            e = e.next;
        }
    }
    return null;
}

在这种情况下,没有阻塞,那么它是如何工作的呢? HashEntry#valuevolatile。那是线程安全的同步点。

CHM-8 的 Node 类是相同的。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

因此,在这种情况下,非空 val 应确保您与 put 之前的操作相关的发生之前的关系。

【讨论】:

【解决方案2】:

文档没有说明会发生同步。例如它声明

[...] 聚合操作如putAllclear,并发 检索可能仅反映某些条目的插入或删除。

换句话说,允许并发使用和提供同步访问是有区别的。

【讨论】:

    【解决方案3】:

    Java 语言规范writes

    如果我们有两个动作 x 和 y,我们写 hb(x, y) 来表示 x 发生在 y 之前。

    • 如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

    • 从对象的构造函数的末尾到该对象的终结器(第 12.6 节)的开头有一条发生前边缘。

    • 如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。

    • 如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。

    defines

    同步动作导致动作上的同步关系,定义如下:

    • 监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。

    • 对 volatile 变量 v(第 8.3.1.4 节)的写入与任何线程对 v 的所有后续读取同步(其中“后续”根据同步顺序定义)。

      李>
    • 启动线程的操作与其启动的线程中的第一个操作同步。

    • 将默认值(零、false 或 null)写入每个变量与每个线程中的第一个操作同步。

      虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。

    • 线程 T1 中的最终操作与另一个线程 T2 中检测到 T1 已终止的任何操作同步。

      T2 可以通过调用 T1.isAlive() 或 T1.join() 来完成此操作。

    • 如果线程 T1 中断线程 T2,则 T1 的中断与任何其他线程(包括 T2)确定 T2 已被中断的任何点同步(通过引发 InterruptedException 或通过调用 Thread.interrupted 或 Thread .isInterrupted)。

    也就是说,读取 volatile 字段会像显式锁一样建立先发生事件。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多