[英]Consequences of updating other key(s) in ConcurrentHashMap#computeIfAbsent
來自ConcurrentHashMap#computeIfAbsent
Javadoc ConcurrentHashMap#computeIfAbsent
說
計算應該簡短,並且不得嘗試更新此映射的任何其他映射。
但是,從我看到的,在mappingFunction
使用remove()
和clear()
方法工作正常。 例如這個
Key element = elements.computeIfAbsent(key, e -> {
if (usages.size() == maxSize) {
elements.remove(oldest);
}
return loader.load(key);
});
在mappingFunction
中使用remove()方法有什么不好的后果呢?
這是一個不好后果的例子:
ConcurrentHashMap<Integer,String> cmap = new ConcurrentHashMap<> ();
cmap.computeIfAbsent (1, e-> {cmap.remove (1); return "x";});
此代碼導致死鎖。
javadoc清楚地解釋了原因:
其他線程在此映射上的某些嘗試更新操作可能 在計算 進行時 被阻止 ,因此計算應該簡短,並且不得嘗試更新此映射的任何其他映射。
您不必忘記ConcurrentHashMap
旨在提供一種使用線程安全Map的方法,而不是像舊HashTable
那樣將舊線程安全Map類作為鎖定。
當對地圖進行修改時,它僅鎖定相關的映射而不是整個映射。
ConcurrentHashMap
是一個哈希表,支持檢索的完全並發和更新的高預期並發性。
computeIfAbsent()
是Java 8中添加的新方法。
如果使用computeIfAbsent()
,也就是說,如果在computeIfAbsent()
的主體中已經鎖定了傳遞給方法的鍵的映射,則鎖定另一個鍵,你輸入一個路徑,你可能會失去ConcurrentHashMap
的目的,最后你將鎖定兩個映射。
想象一下,如果你在computeIfAbsent()
鎖定更多的映射並且該方法根本不短,那么問題就出現了。 地圖上的並發訪問會變慢。
因此, computeIfAbsent()
的javadoc通過提醒ConcurrentHashMap
的原理來強調這個潛在的問題:保持簡單和快速。
以下是說明問題的示例代碼。
假設我們有一個ConcurrentHashMap<Integer, String>
的實例。
我們將啟動兩個使用它的線程:
thread1
,使用鍵1
調用computeIfAbsent()
thread2
,使用鍵2
調用computeIfAbsent()
thread1
執行足夠快的任務,但它不遵循的提醒computeIfAbsent()
的Javadoc:它更新鍵2
在computeIfAbsent()
即其中一個的另一映射在方法(的當前上下文中使用是關鍵1
)。
thread2
執行足夠長的任務。 它通過遵循javadoc的建議來調用帶有鍵2
的computeIfAbsent()
:它不會更新它的實現中的任何其他映射。
為了模擬長任務,我們可以使用Thread.sleep()
方法和5000
作為參數。
對於這種特定情況,如果thread2
在thread1
之前thread1
,則調用map.put(2, someValue);
在thread1
而將被阻塞thread2
不返回的computeIfAbsent()
用於鎖定的鍵的映射2
。
最后,我們得到一個ConcurrentHashMap
實例,它在5秒內computeIfAbsent()
鍵2
的映射,同時使用鍵1
的映射調用computeIfAbsent()
。
它具有誤導性,無效並且與ConcurrentHashMap
意圖和computeIfAbsent()
描述computeIfAbsent()
,意圖是計算當前鍵的值:
如果指定的鍵尚未與值關聯,則嘗試使用給定的映射函數計算其值並將其輸入此映射,除非null
示例代碼:
import java.util.concurrent.ConcurrentHashMap;
public class BlockingCallOfComputeIfAbsentWithConcurrentHashMap {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
Thread thread1 = new Thread() {
@Override
public void run() {
map.computeIfAbsent(1, e -> {
String valueForKey2 = map.get(2);
System.out.println("thread1 : get() returns with value for key 2 = " + valueForKey2);
String oldValueForKey2 = map.put(2, "newValue");
System.out.println("thread1 : after put() returns, previous value for key 2 = " + oldValueForKey2);
return map.get(2);
});
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
map.computeIfAbsent(2, e -> {
try {
Thread.sleep(5000);
} catch (Exception e1) {
e1.printStackTrace();
}
String value = "valueSetByThread2";
System.out.println("thread2 : computeIfAbsent() returns with value for key 2 = " + value);
return value;
});
}
};
thread2.start();
Thread.sleep(1000);
thread1.start();
}
}
作為輸出,我們總是得到:
thread1:get()返回值2的值= null
thread2:computeIfAbsent()返回key 2 = valueSetByThread2的值
thread1:put()返回后,key 2的前一個值= valueSetByThread2
這是快速編寫的,因為ConcurrentHashMap
上的讀取沒有阻塞:
thread1:get()返回值2的值= null
但是這個 :
thread1:put()返回后,key 2的前一個值= valueSetByThread2
僅當threadI返回computeIfAbsent()
時才輸出。
這樣的建議有點像不走在路中間的建議。 你可以做到,你可能不會被汽車擊中; 如果你看到汽車即將到來,你也可以走開。
但是,如果你剛剛在人行道(人行道)上停留,那么你會更安全。
如果API文檔告訴您不要做某事,那么當然沒有什么能阻止您這樣做。 你可能會嘗試這樣做,並發現沒有不良后果,至少在你測試的有限環境中。 您甚至可以深入了解建議的確切原因; 您可以仔細檢查源代碼並證明它在您的用例中是安全的。
但是,API的實現者可以在API文檔描述的合同約束內自由更改實現。 他們可能會做出改變,阻止您明天的代碼工作,因為他們沒有義務保留他們明確警告不要使用的行為。
所以,回答你的問題是什么可能是壞的結果:字面上任何東西(好吧,任何正常完成或拋出RuntimeException
); 並且您不一定會在一段時間內或在不同的JVM上觀察到相同的后果。
留在人行道上:不要做文件告訴你的事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.