[英]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。
這樣的代碼可能是正確的,但應該仔細審查和記錄,並且可能會使以后必須維護代碼的人感到困惑。
如果您必須為每次更新更改許多節點,則可能是您使用了錯誤的數據結構。 檢查樹的並發實現。 持久化集合(提供不變性和快速更新)似乎是理想的。
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.Hashtable
和Collections.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 找不到 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 在下面提到的限制。 由於特定問題的性質,在我的情況下不適用:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.