天天看點

ConcurrentMap.putIfAbsent(key,value) 用法

業務上經常會遇到有這種場景,全局維護一個并發的ConcurrentMap, Map的每個Key對應一個對象,這個對象需要隻建立一次。如果Map中該key對應的value不存在則建立,否則直接傳回。

我們先看一下代碼:

public static Locale getInstance(String language, String country,  
            String variant) {  
        //...  
        String key = some_string;  
        Locale locale = map.get(key);  
        if (locale == null) {  
            locale = new Locale(language, country, variant);  
            map.put(key, locale);  
        }  
        return locale;  
    }  
           

 這段代碼要做的事情是:

  1. 調用 map.get(key) 方法,判斷 map 裡面是否有該 key 對應的 value (Locale 對象)。
  2. 如果傳回 null,表示 map 裡面沒有要查找的 key-value mapping。new 一個 Locale 對象,并把 new 出來的這個對象與 key 一起放入 map。
  3. 最後傳回新建立的 Locale 對象

我們期望每次調用 getInstance 方法時要保證相同的 key 傳回同一個 Local 對象引用。這段代碼能實作這個需求嗎?

答案是:在單線程環境下可以滿足要求,但是在多線程環境下會存線上程安全性問題,即不能保證在并發的情況相同的 key 傳回同一個 Local 對象引用。

這是因為在上面的代碼裡存在一個習慣被稱為 put-if-absent 的操作 [1],而這個操作存在一個 race condition:

if (locale == null) {  
    locale = new Locale(language, country, variant);  
    map.put(key, locale);  
}
           

因為在某個線程做完 locale == null 的判斷到真正向 map 裡面 put 值這段時間,其他線程可能已經往 map 做了 put 操作,這樣再做 put 操作時,同一個 key 對應的 locale 對象被覆寫掉,最終 getInstance 方法傳回的同一個 key 的 locale 引用就會出現不一緻的情形。是以對 Map 的 put-if-absent 操作是不安全的(thread safty)。

為了解決這個問題,java 5.0 引入了 ConcurrentMap 接口,在這個接口裡面 put-if-absent 操作以原子性方法 putIfAbsent(K key, V value) 的形式存在。正如 javadoc 寫的那樣:

putIfAbsent方法主要是在向ConcurrentHashMap中添加鍵—值對的時候,它會先判斷該鍵值對是否已經存在。

  • 如果不存在(新的entry),那麼會向map中添加該鍵值對,并傳回null。 
  • 如果已經存在,那麼不會覆寫已有的值,直接傳回已經存在的值。 

對上面方法進行改造:

public static Locale getInstance(String language, String country,  
            String variant) {  
        //...  
        String key = some_string;  
        Locale locale = map.get(key);  
        if (locale == null) {  
            locale = new Locale(language, country, variant);  
            map.putIfAbsent(key, locale);  
        }  
        return locale;  
    }  
           

這段代碼使用了 Map 的 concurrent 形式(ConcurrentMap、ConcurrentHashMap),并簡單的使用了語句map.putIfAbsent(key, locale) 。這同樣不能保證相同的 key 傳回同一個 Locale 對象引用。

這裡的錯誤出在忽視了 putIfAbsent 方法是有傳回值的,并且傳回值很重要。

是以,使用 putIfAbsent 方法時切記要對傳回值進行判斷。

public static Locale getInstance(String language, String country,  
            String variant) {
        //...  
        String key = some_string;  
        Locale locale = map.get(key);  
        if (locale == null) {  
            locale = new Locale(language, country, variant);  
            Locale tmp = map.putIfAbsent(key, locale);
			if (tmp != null) {
				locale = tmp;
			}
        }  
        return locale;  
}  
           

【執行個體1】

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {

    public static void main(String[] args) {

        //測試一下currentHashMap.putIfAbsent()

        Map<Long, String> clientMap = new ConcurrentHashMap<>();

        System.out.println("首先列印空的clientMap");
        System.out.println("clientMap: " + clientMap);
        System.out.println();

        //在空的clientMap中添加一個新的記錄
        System.out.println("在空的clientMap中添加一個新的記錄");
        System.out.println("添加之前的clientMap: " + clientMap);
        long netId = 1234567L;
        String str1 = "michael";
        String result = clientMap.putIfAbsent(netId, str1);
        System.out.println("添加之後的clientMap: " + clientMap);
        System.out.println("檢視傳回值result: " + result);
        System.out.println();

        //重複添加
        System.out.println("重複添加上一次的記錄");
        System.out.println("添加之前的clientMap: " + clientMap);
        String result2 = clientMap.putIfAbsent(netId, str1);
        System.out.println("添加之後的clientMap: " + clientMap);
        System.out.println("檢視傳回值result: " + result2);
        System.out.println();

    }
}
           
ConcurrentMap.putIfAbsent(key,value) 用法