簡體   English   中英

更新ConcurrentHashMap中的其他鍵的后果#computeIfAbsent

[英]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:它更新鍵2computeIfAbsent()即其中一個的另一映射在方法(的當前上下文中使用是關鍵1 )。

thread2執行足夠長的任務。 它通過遵循javadoc的建議來調用帶有鍵2computeIfAbsent() :它不會更新它的實現中的任何其他映射。
為了模擬長任務,我們可以使用Thread.sleep()方法和5000作為參數。

對於這種特定情況,如果thread2thread1之前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.

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