java - ConcurrentHashMap 中的可变语义

java - volatile semantics in ConcurrentHashMap

在JDK8的ConcurrentHashMap中,tabAtsetTabAt方法用于提供read/write中bin第一个元素的volatileNode<K,V>[] table。但是,作者评论说:

Note that calls to setTabAt always occur within locked regions, and so in principle require only release ordering, not full volatile semantics, but are currently coded as volatile writes to be conservative.

我想知道释放顺序是否意味着[=17=保证的先发生关系(监视器的解锁发生在同一监视器的每个后续锁定之前) ].如果是这样,为什么 setTabAt 被认为是 保守的 ,而不是强制性的,因为对 tabAt 的调用不仅存在于内部,也存在于外部 synchronized块?例如:

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

如有任何帮助,我们将不胜感激。

在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 release,将 tabAt 视为 get acquire

release这里的意思是release/acquire semantics, which I sort of talked about in this answer。关键是 volatile 在某些情况下(顺序一致性)写入“太多”,就像这里的那样。

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

Versions of putReferenceVolatile... that do not guarantee immediate visibility of the store to other threads...

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