簡體   English   中英

雙重檢查鎖定是否與Java中的最終Map一起使用?

[英]Does double-checked locking work with a final Map in Java?

我正在嘗試實現一個線程安全的Map緩存,我希望緩存的Strings被懶惰地初始化。 這是我在實施中的第一次傳遞:

public class ExampleClass {

    private static final Map<String, String> CACHED_STRINGS = new HashMap<String, String>();

    public String getText(String key) {

        String string = CACHED_STRINGS.get(key);

        if (string == null) {

            synchronized (CACHED_STRINGS) {

                string = CACHED_STRINGS.get(key);

                if (string == null) {
                    string = createString();
                    CACHED_STRINGS.put(key, string);
                }
            }
        }

        return string;
    }
}

編寫完這段代碼后,Netbeans警告我“雙重檢查鎖定”,所以我開始研究它。 我發現“雙重鎖定已破損”聲明並閱讀它,但我不確定我的實施是否成為它提到的問題的犧牲品。 看起來本文中提到的所有問題都與使用synchronized塊中的new運算符進行對象實例化有關。 我沒有使用new運算符,並且字符串是不可變的,所以我不確定文章是否與這種情況相關。 這是一種在HashMap緩存字符串的線程安全方法嗎? 線程安全性取決於createString()方法中采取的操作嗎?

不,這是不正確的,因為第一次訪問是在同步塊的一側完成的。

這有點落到了如何實現getput 你必須記住它們不是原子操作

例如,如果它們是這樣實現的:

public T get(string key){
    Entry e = findEntry(key);
    return e.value;
}

public void put(string key, string value){
    Entry e = addNewEntry(key);
    //danger for get while in-between these lines
    e.value = value;
}

private Entry addNewEntry(key){
   Entry entry = new Entry(key, ""); //a new entry starts with empty string not null!
   addToBuckets(entry); //now it's findable by get
   return entry; 
}

現在,當put操作仍在進行時, get可能不會返回null ,並且整個getText方法可能返回錯誤的值。

該示例有點復雜,但您可以看到代碼的正確行為依賴於map類的內部工作方式。 這不好。

雖然您可以查看該代碼,但您無法考慮編譯器,JIT和處理器優化以及內聯哪些可以有效地改變操作順序,就像我選擇編寫該地圖實現的古怪但正確的方式一樣。

考慮使用並發散列映射和方法Map.computeIfAbsent() ,如果映射中不存在鍵,則使用函數調用以計算默認值。

Map<String, String> cache = new ConcurrentHashMap<>(  );
cache.computeIfAbsent( "key", key -> "ComputedDefaultValue" );

Javadoc:如果指定的鍵尚未與值關聯,則嘗試使用給定的映射函數計算其值,並將其輸入此映射,除非為null。 整個方法調用是以原子方式執行的,因此每個鍵最多應用一次該函數。 其他線程在此映射上的某些嘗試更新操作可能在計算進行時被阻止,因此計算應該簡短,並且不得嘗試更新此映射的任何其他映射。

非平凡問題域:

並發很容易做,很難正確完成。

緩存很容易做,很難正確完成。

在沒有對問題領域及其許多微妙的副作用和行為的深入理解的情況下,兩者都是加密的類別。

結合它們,你會遇到一個比任何一個問題都難度更大的問題。

這是一個非常重要的問題,您的天真實現無法以無錯誤的方式解決。 如果沒有檢查和序列化任何訪問,您使用的HashMap將不會線程安全,它將不會具有高性能並且會導致大量爭用,這將導致大量阻塞和延遲,具體取決於使用情況。

實現延遲加載緩存的正確方法是使用Guava CacheCache Loader之類的東西,它透明地處理所有並發和緩存競爭條件。 粗略瀏覽源代碼可以看出它們是如何做到的。

不,ConcurrentHashMap也無濟於事。

回顧:雙重檢查習慣通常是關於為變量/字段分配新實例; 它被破壞是因為編譯器可以重新排序指令,這意味着可以使用部分構造的對象來分配字段。

對於您的設置,您有一個明顯的問題:map.get()對於可能正在發生的put()是不安全的,因此可能會重新表格。 使用並發哈希映射僅修復但不存在誤報的風險(您認為地圖沒有條目但實際上正在制作)。 問題不是部分構建的對象,而是重復工作。

至於可避免的guava cacheloader:這只是一個lazy-init回調,你給地圖,所以它可以創建對象,如果丟失。 這與把所有'if null'代碼放在鎖中基本相同,這肯定不會比舊的直接同步更快。 (唯一有意義的是,使用緩存加載器是為了插入這樣的丟失對象的工廠,同時將地圖傳遞給不知道如何制作丟失對象且不想被告知如何的類。

暫無
暫無

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

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