簡體   English   中英

java - ConcurrentHashMap 中的易失語義

[英]java - volatile semantics in ConcurrentHashMap

在 JDK 8 的ConcurrentHashMap中,方法tabAtsetTabAt用於提供對Node<K,V>[] table中 bin 的第一個元素的易失性讀/寫。 然而,作者評論說:

請注意,對setTabAt的調用始終發生在鎖定區域內,因此原則上只需要發布順序,而不需要完整的 volatile 語義,但目前編碼為 volatile 寫入以保持保守。

我想知道這里的發布順序是否意味着由synchronized保證的發生前關系(監視器的解鎖發生在同一監視器的每個后續鎖定之前)。 如果是這樣,為什么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 據我了解,監視器鎖已經處理了線程之間的 memory 可見性,例如:

  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. 由於 Thread-A 和 Thread-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意味着釋放/獲取語義,我在這個答案中談到了這一點。 關鍵是volatile寫入在某些情況下(順序一致性)“做得太多”,就像這里的情況一樣。

源代碼(jdk-13)中有評論說(關於putReferenceRelease包括)這是一個“弱(er)易失性”:

putReferenceVolatile 的版本......不保證商店對其他線程的立即可見性......

讀取線程也使用相同的鎖時, synchronized部分僅提供 memory 可見性保證; 否則所有賭注都取消。 看來這是您缺少的部分。 這是一個更具描述性的答案,解釋了synchronized部分如何被嚴重破壞。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM