【问题标题】:java - volatile semantics in ConcurrentHashMapjava - ConcurrentHashMap 中的易失语义
【发布时间】:2020-07-07 08:07:13
【问题描述】:

在 JDK 8 的 ConcurrentHashMap 中,tabAtsetTabAt 方法用于提供对 Node<K,V>[] table 中 bin 的第一个元素的易失性读/写。然而,作者认为:

请注意,对setTabAt 的调用总是发生在锁定区域内,因此原则上只需要发布顺序,而不需要完整的 volatile 语义,但目前编码为 volatile 写入是保守的。

我想知道这里的 发布顺序 是否意味着 synchronized 保证的发生前关系(监视器的解锁发生在同一监视器的每个后续锁定之前)。如果是这样,考虑到对tabAt 的调用不仅存在于synchronized 块内部,而且存在于synchronized 块之外,为什么setTabAt 被认为是保守的,而不是强制性的?例如:

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //...
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // -> tabAt called here, outside the synchronized block
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    // -> tabAt called here, inside the synchronized block
                    if (tabAt(tab, i) == f) {
                        // do insertion...
                    }
                }
            }        
        }
    }

另一个问题是,在上面的代码中,是否需要在 synchronized 块内调用 tabAt?在我的理解中,监视器锁已经处理了线程之间的内存可见性,例如:

  1. 假设 bin 中只有一个元素,比如 Node-0
  2. Thread-A 想要将 Node-1 插入 bin 中,就在它通过在 synchronized 块外调用 tabAt 找到的 Node-0 之后
  3. 但在 Thread-A 可以锁定 Node-0 之前,Thread-B 会锁定 Node-0 并将其删除(通过调用 setTabAt
  4. Thread-B 释放锁后,Thread-A 获取 Node-0 的锁
  5. 由于线程A和线程B之间的happens-before关系由监视器锁保证,在这种情况下,在我看来没有必要调用tabAt(这反过来又调用Unsafe.getObjectVolatile)访问并重新检查元素。

任何帮助将不胜感激。

【问题讨论】:

    标签: java concurrency volatile concurrenthashmap java-memory-model


    【解决方案1】:

    在java-8中,你提到的那个方法被定义为:

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
    

    以jdk-13为例,已经是release

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
    }
    

    据我所知,应该与:

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
    }
    

    因此您可以将setTabAt 视为set releasetabAt 视为get acquire

    release 这里的意思是release/acquire semantics,我在这个answer 中谈到过。关键是volatile 在某些情况下(顺序一致性)写“太多”了,就像这里的那个。

    (jdk-13 的)源代码中有注释说(关于 putReferenceRelease 包括)这是一个“弱易失性”:

    putReferenceVolatile 的版本...不保证商店对其他线程的立即可见性...

    synchronized 部分仅在 reading 线程也使用相同的锁时提供内存可见性保证;否则所有赌注都取消。看来这是您缺少的部分。 Here is a more descriptive answer 解释了 synchronized 部分是如何被严重破坏的。

    【讨论】:

    • 非常感谢您的回答。根据您提供的文章链接,我花了很多天试图完全理解 volatile 语义和 release/acquire 语义之间的区别......最后我进入了this article,我认为这对我来说是最清楚的。
    • 根据上面提到的文章,Sequentially-consistent ordering“不仅以与释放/获取顺序相同的方式对内存进行排序,而且还建立了所有原子的单个总修改顺序如此标记的操作...对于多个生产者-多个消费者的情况,所有消费者必须观察所有生产者以相同顺序发生的操作,可能需要顺序排序。”但是我想知道Java中的volatile是否也保证了这种修改顺序?谢谢!
    • @yangty89 是的,java volatile 确实提供了顺序一致性。 Here is more about this
    猜你喜欢
    • 2010-11-02
    • 2011-01-03
    • 1970-01-01
    • 2011-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-24
    相关资源
    最近更新 更多