週末的時候,有一位小夥伴提了一些關於 ConcurrentHashMap
的問題,都是他最近面試遇到的。原提問如下:
整個提問看著非常複雜,其實歸納來說就是兩個問題:
ConcurrentHashMap
為什麼 key 和 value 不能為 null?ConcurrentHashMap
能保證複合操作的原子性嗎?下面我會以此提供這兩個問題的詳細答案,希望對你有幫助。
ConcurrentHashMap
的 key 和 value 不能為 null 主要是為了避免二義性。null 是一個特殊的值,表示沒有物件或沒有參照。如果你用 null 作為鍵,那麼你就無法區分這個鍵是否存在於 ConcurrentHashMap
中,還是根本沒有這個鍵。同樣,如果你用 null 作為值,那麼你就無法區分這個值是否是真正儲存在 ConcurrentHashMap
中的,還是因為找不到對應的鍵而返回的。
拿 get 方法取值來說,返回的結果為 null 存在兩種情況:
這也就是二義性的由來。
具體可以參考 ConcurrentHashMap 原始碼分析 。
多執行緒環境下,存在一個執行緒操作該 ConcurrentHashMap
時,其他的執行緒將該 ConcurrentHashMap
修改的情況,所以無法通過 containsKey(key)
來判斷否存在這個鍵值對,也就沒辦法解決二義性問題了。
與此形成對比的是,HashMap
可以儲存 null 的 key 和 value,但 null 作為鍵只能有一個,null 作為值可以有多個。如果傳入 null 作為引數,就會返回 hash 值為 0 的位置的值。單執行緒環境下,不存在一個執行緒操作該 HashMap 時,其他的執行緒將該 HashMap
修改的情況,所以可以通過 contains(key)
來做判斷是否存在這個鍵值對,從而做相應的處理,也就不存在二義性問題。
也就是說,多執行緒下無法正確判定鍵值對是否存在(存在其他執行緒修改的情況),單執行緒是可以的(不存在其他執行緒修改的情況)。
如果你確實需要在 ConcurrentHashMap 中使用 null 的話,可以使用一個特殊的靜態空物件來代替 null。
public static final Object NULL = new Object();
最後,再分享一下 ConcurrentHashMap
作者本人 (Doug Lea)對於這個問題的回答:
The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if
map.get(key)
returnsnull
, you can't detect whether the key explicitly maps tonull
vs the key isn't mapped. In a non-concurrent map, you can check this viamap.contains(key)
, but in a concurrent one, the map might have changed between calls.
翻譯過來之後的,大致意思還是單執行緒下可以容忍歧義,而多執行緒下無法容忍。
ConcurrentHashMap
是執行緒安全的,意味著它可以保證多個執行緒同時對它進行讀寫操作時,不會出現資料不一致的情況,也不會導致 JDK1.7 及之前版本的 HashMap
多執行緒操作導致死迴圈問題。但是,這並不意味著它可以保證所有的複合操作都是原子性的,一定不要搞混了!
複合操作是指由多個基本操作(如put
、get
、remove
、containsKey
等)組成的操作,例如先判斷某個鍵是否存在containsKey(key)
,然後根據結果進行插入或更新put(key, value)
。這種操作在執行過程中可能會被其他執行緒打斷,導致結果不符合預期。
例如,有兩個執行緒 A 和 B 同時對 ConcurrentHashMap
進行復合操作,如下:
// 執行緒 A
if (!map.containsKey(key)) {
map.put(key, value);
}
// 執行緒 B
if (!map.containsKey(key)) {
map.put(key, anotherValue);
}
如果執行緒 A 和 B 的執行順序是這樣:
那麼最終的結果是 (key, value),而不是預期的 (key, anotherValue)。這就是複合操作的非原子性導致的問題。
那如何保證 ConcurrentHashMap
複合操作的原子性呢?
ConcurrentHashMap
提供了一些原子性的複合操作,如 putIfAbsent
、compute
、computeIfAbsent
、computeIfPresent
、merge
等。這些方法都可以接受一個函數作為引數,根據給定的 key 和 value 來計算一個新的 value,並且將其更新到 map 中。
上面的程式碼可以改寫為:
// 執行緒 A
map.putIfAbsent(key, value);
// 執行緒 B
map.putIfAbsent(key, anotherValue);
或者:
// 執行緒 A
map.computeIfAbsent(key, k -> value);
// 執行緒 B
map.computeIfAbsent(key, k -> anotherValue);
很多同學可能會說了,這種情況也能加鎖同步呀!確實可以,但不建議使用加鎖的同步機制,違背了使用 ConcurrentHashMap
的初衷。在使用 ConcurrentHashMap
的時候,儘量使用這些原子性的複合操作方法來保證原子性。