簡體   English   中英

一個ConcurrentHashMap可以在Java外同步嗎?

[英]Can a ConcurrentHashMap be externally synchronized in Java?

我正在使用 ConcurrentHashMap,我需要在計算一個尚不存在的新元素時迭代它的所有元素,並可能對相同的 map 進行一些其他修改。

我希望這些操作是原子的,並阻止 ConcurrentHashMap 以防止獲得從並發派生的異常。

我編寫的解決方案是將 ConcurrentHashMap object 與自身同步為鎖,但 Sonar 報告了一個主要問題,所以我不知道該解決方案是否正確

建議代碼:

對原文的修改

public class MyClass<K, V> {
    ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();

    public V get(K key) {
        return map.computeIfAbsent(key, this::calculateNewElement);
    }

    protected V calculateNewElement(K key) {
        V result;
        // the following line throws the sonar issue:
        synchronized(map) {
            // calculation of the new element (assignating it to result)
            // with iterations over the whole map
            // and possibly with other modifications over the same map
        }
        return result;
    }
}

此代碼觸發聲納主要問題:

多線程 - 在 util.concurrent 實例上執行的同步

發現錯誤:JLM_JSR166_UTILCONCURRENT_MONITORENTER

此方法對 object 執行同步,它是 class 的實例,來自 java.util.concurrent package(或其子類)。 這些類的實例有自己的並發控制機制,這些機制與 Java 關鍵字 synchronized 提供的同步正交。 例如,同步 AtomicBoolean 不會阻止其他線程修改 AtomicBoolean。

這樣的代碼可能是正確的,但應該仔細審查和記錄,並且可能會使以后必須維護代碼的人感到困惑。

如果您必須為每次更新更改許多節點,則可能是您使用了錯誤的數據結構。 檢查樹的並發實現。 持久化集合(提供不變性和快速更新)似乎是理想的。

提供了一種用於原子更新的方法: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ConcurrentHashMap.html#compute(K,java .util.function.BiFunction)

ConcurrentHashMap 被構建為允許高度並發訪問。 (請參閱描述其內部工作原理的這篇文章)如果使用提供的方法(例如計算、computeIfPresent 等)對條目進行更新,則應僅鎖定條目所在的段,而不是整個條目。

當您鎖定整個 map 以進行更新時,您無法從使用這種專用數據結構中獲益。 這就是 Sonar 所抱怨的。

還有一個問題是讀者也必須進行鎖定,更新程序線程並不是唯一需要鎖定的線程。 這種事情是CHM最初被發明的原因。

https://www.amazon.com.tr/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601

“並發對象不支持‘客戶端鎖定’”

您可以對同步的 collections 等執行客戶端鎖定。

final List<Type> synchronizedList = Collections.synchronizedList(new ArrayList<>()); //do not use another reference to internal array list and access the list using through synchronizedList reference.

在這種情況下,您可以使用;

synchronized(synchronizedList){
  //do something with synchronized list.
}

注意:這可能表現不佳,即引入可伸縮性問題,因為代碼是高度序列化的。 (阿姆達爾定律)。

並發對象是為可伸縮性而設計的。 也許您可以將 map 的快照拍攝到另一個“本地”collections 並對其進行操作。 或者你可以直接使用 map 而不進行任何同步。 (在這種情況下,可以添加或刪除一些新元素,您的迭代器可能會或可能不會反映這些更改)

“ConcurrentHashMap 與其他並發 collections 一起,通過提供不拋出 ConcurrentModificationException 的迭代器進一步改進了同步集合類,從而消除了在迭代期間鎖定集合的需要。ConcurrentHashMap 返回的迭代器是弱一致的而不是快速失敗的.弱一致性迭代器可以容忍並發修改,遍歷構造迭代器時存在的元素,並且可能(但不保證)反映迭代器構造后對集合的修改。

ConcurrentHashMap 上有一些操作允許您對特定鍵執行原子操作,如計算、computeIfAbsent、computeIfPresent。 https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html

您可以將同步和訪問 map 的組合替換為“常規”集合並使用ReadWriteLock (例如java.util.concurrent.locks.ReentrantReadWriteLock

看這部分對java.util.concurrent package的描述:

與此 package 中的某些類一起使用的“並發”前綴是一種簡寫,表示與類似的“同步”類的一些差異。 例如java.util.HashtableCollections.synchronizedMap(new HashMap())是同步的。 但是ConcurrentHashMap是“並發”的。 並發集合是線程安全的,但不受單個排他鎖的約束。 在 ConcurrentHashMap 的特殊情況下,它安全地允許任意數量的並發讀取以及大量並發寫入。 當您需要通過單個鎖阻止對集合的所有訪問時,“同步”類會很有用,但代價是可擴展性較差。 在其他需要多個線程訪問公共集合的情況下,“並發”版本通常更可取。 當 collections 未共享或僅在持有其他鎖時才可訪問時,不同步的 collections 更可取。

ReadWriteLock的文檔:

ReadWriteLock維護一對關聯的鎖,一個用於只讀操作,一個用於寫入。 只要沒有寫者,讀鎖可以同時被多個讀者線程持有。 寫鎖是獨占的。

“可重入”實現模仿synchronized塊的行為:(來自ReentrantLock的文檔)

一種可重入互斥鎖,其基本行為和語義與使用同步方法和語句訪問的隱式監視器鎖相同,但具有擴展功能。

您的代碼可能如下所示:

public class MyClass<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public V get(K key) {
        readLock.lock();
        try {
            return map.computeIfAbsent(key, this::calculateNewElement);
        } finally {
            readLock.unlock();
        }
    }

    protected V calculateNewElement(K key) {
        readLock.unlock();
        writeLock.lock();
        try {
            V result;
            // calculation of the new element (assigning it to result)
            // with iterations over the whole map
            // and possibly with other modifications over the same map
            return result;
        } finally {
            writeLock.unlock();
        }
    }

    public V put(K key, V value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

通過這種實現,讀取在寫入發生時被阻塞,反之亦然。 多個讀取仍然可以同時進行,但寫入是排他的。

但是你必須注意 map 不會“逃脫” object 並且以某種方式被訪問 - 同樣在 class 內部你必須用鎖保護對 map 的所有訪問。

ReentrantReadWriteLock的 JavaDocs 為您提供了示例和您應該注意的一些條件(例如鎖的大小限制)。

感謝所有答案,我終於可以編寫一個解決方案。

public class MyClass<K, V> {
    ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();

    public V get(K key) {
        V result = map.get(key);
        if(result == null) {
            result = calculateNewElement(key);
        }
        return 
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    protected synchronized V calculateNewElement(K key) {
        V result = map.get(key);
        if(result == null) {
            // calculation of the new element (assignating it to result)
            // with iterations over the whole map
            // and possibly with other modifications over the same map

            put(key, result);
        }
        return result;
    }
}

我將更多地描述這個問題:

解決方案試圖解決的特定問題的描述:

  • K 有兩個屬性,它們是 Class<?> 類型的對象(我們稱它們為 originClass 和 destinationClass),
  • V 是從具有originClass (或其超類之一)的 Pojo 到具有destinationClass (或其派生類之一)的 Pojo 的翻譯器,因此它符合翻譯 originClass -> destinationClass

當對於特定 K 找不到 V 時,則

calculateNewElement function 嘗試尋找 originClass 和 destinationClass 之間的非直接路徑,

(這意味着我們的 originClass 和 destinationClass(0) 可能有一個鍵 (K0)

另一個鍵 (K1) 的 origin(1) 等於 destinationClass(0) 和 destinationClass(1),它是我們的 destinationClass 的派生 class)。

這可能會導致一個新的 V,它是鍵的組合:

K0 ( originClass(0) = originClass, destinationClass(0) ) (V0) -->

K1 ( originClass(1) = destinationClass(0), destinationClass(1) (destinationClass 的派生 class) ) (V1) -->

K2 ( originClass(2) = destinationClass(1), destinationClass(2) = destinationClass) (地圖中不存在的直接翻譯)

我們可以這樣將 K1 和 K2 加入一個 new_K 中:

put( new_K(originClass(1), destinationClass), V1 ) // 這是放在 calculateNewElement 中的新的不同鍵

那么由 calculateNewElement 創建的新 V(我們原來的 K)將是 K0 和 new_K 的組合:

V v = new VCombination(K0, new_K),也將由我們的calculateNewElement function

在我的例子中,put function 很少被調用(僅在初始化期間),同步是可以接受的。

這種情況符合 Holger 在下面提到的限制。 由於特定問題的性質,在我的情況下不適用:

  • 只有當“中途”元素還不完全存在時,calculateNewElement function 才會將其放入 map
  • 新的元素(是已有元素的組合)只需要組合的元素存在於map中即可,所以不允許從Map中移除元素(只能清空)

暫無
暫無

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

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