Java 8 ConcurrentHashMap

21

我注意到Java 8中的ConcurrentHashMap已经完全重写,以更加"无锁"的方式实现。我浏览了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;
}
问题:
如何在一个线程中看到其他线程对哈希映射表所做的修改,因为代码不在同步范畴之下(这会强制执行先行发生关系)?
注意:整个ConcurrentHashMap是一个表的包装器:transient volatile Node<K,V>[] table; 因此,table是一个对数组的volatile引用,而不是一个对易失元素数组的引用!这意味着如果有人在此数组中更新一个元素,其他线程将无法看到修改。

2
你可能会发现这是一篇(额外的)好文章:http://www.javaspecialists.eu/archive/Issue235.html - GhostCat
1
ConcurrentHashMap 实际上使用 Unsafe 从数组元素中执行 volatile 读取。这就是方法 tabAt 的作用。 - Radiodef
1
只是澄清一下,@Radiodef,“Unsafe”的同步在“put”而不是读取上。 - John Vint
3个回答

14

简短回答

Node#valvolatile 的,这可以确保 happens-before 排序。

详细回答

synchronized 并不是实现线程安全的必要条件,而是保证系统线程安全的工具之一。你需要考虑对 ConcurrentHashMap 的整个操作集合来确定其线程安全性。

了解原始的 ConcurrentHashMap 也很有用,它是非阻塞的。请注意,在 Java 8 之前的 CHM get。

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之前的操作与之存在happens-before关系。


有关忽略此内容的更多信息,请阅读我在https://dev59.com/nm445IYBdhLWcg3wLXIK上的答案。 - John Vint

5
文档未说明发生同步。例如,它声明:
[...] 聚合操作(例如putAllclear), 并发检索可能反映仅插入或删除某些条目。
换句话说,允许并发使用与提供同步访问之间存在差异。

0

Java语言规范写道

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

  • 如果x和y是同一线程的动作,并且x在程序顺序中出现在y之前,则hb(x,y)。

  • 对于一个对象的构造函数的结尾到该对象的finalizer(§12.6)的开始,存在一个happens-before边缘。

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

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

定义了以上内容。

同步操作会在动作上引入同步关系,定义如下:
- 在监视器 m 上的解锁操作与随后在 m 上进行的所有锁定操作(其中“随后”根据同步顺序定义)同步。 - 对易失性变量 v(§8.3.1.4)的写入与任何线程对 v 的随后读取同步(其中“随后”根据同步顺序定义)。 - 启动线程的操作与启动的线程中的第一个操作同步。 - 将默认值(零、false 或 null)写入每个变量与每个线程中的第一个操作同步。
尽管在分配包含变量的对象之前向变量写入默认值可能看起来有点奇怪,但从概念上讲,每个对象都是在程序开始时创建的,并具有其默认初始化值。
- 线程 T1 中的最终操作与检测到 T1 已终止的另一个线程 T2 中的任何操作同步。T2 可以通过调用 T1.isAlive() 或 T1.join() 来实现这一点。 - 如果线程 T1 中断线程 T2,则 T1 的中断与任何其他线程(包括 T2)确定 T2 已被中断的任何点同步(通过抛出 InterruptedException 或调用 Thread.interrupted 或 Thread.isInterrupted)。

也就是说,读取一个 volatile 字段会建立 happens-before 关系,就像显式锁一样。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接