美團面試拷打:ConcurrentHashMap 為何不能插入 null?HashMap 為何可以?

JavaGuide 官方網站:javaguide.cn
周末的時候, 知識星球有一位小伙伴提了一些關于 ConcurrentHashMap 的問題,都是他最近面試遇到的。原提問如下(星球原貼地址:https://t.zsxq.com/11jcuezQs ):
整個提問看著非常復雜,其實歸納來說就是兩個問題:
-
ConcurrentHashMap為什么 key 和 value 不能為 null? -
ConcurrentHashMap能保證復合操作的原子性嗎?
下面我會以此提供這兩個問題的詳細答案,希望對你有幫助。
ConcurrentHashMap 為什么 key 和 value 不能為 null?
ConcurrentHashMap 的 key 和 value 不能為 null 主要是為了避免二義性。null 是一個特殊的值,表示沒有對象或沒有引用。如果你用 null 作為鍵,那么你就無法區(qū)分這個鍵是否存在于 ConcurrentHashMap 中,還是根本沒有這個鍵。同樣,如果你用 null 作為值,那么你就無法區(qū)分這個值是否是真正存儲在 ConcurrentHashMap 中的,還是因為找不到對應的鍵而返回的。
拿 get 方法取值來說,返回的結果為 null 存在兩種情況:
-
值沒有在集合中 ; -
值本身就是 null。
這也就是二義性的由來。
具體可以參考 ConcurrentHashMap 源碼分析( https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html)這篇文章。
多線程環(huán)境下,存在一個線程操作該 ConcurrentHashMap 時,其他的線程將該 ConcurrentHashMap 修改的情況,所以無法通過 containsKey(key) 來判斷否存在這個鍵值對,也就沒辦法解決二義性問題了。
與此形成對比的是,HashMap 可以存儲 null 的 key 和 value,但 null 作為鍵只能有一個,null 作為值可以有多個。如果傳入 null 作為參數(shù),就會返回 hash 值為 0 的位置的值。單線程環(huán)境下,不存在一個線程操作該 HashMap 時,其他的線程將該 HashMap 修改的情況,所以可以通過 contains(key)來做判斷是否存在這個鍵值對,從而做相應的處理,也就不存在二義性問題。
也就是說,多線程下無法正確判定鍵值對是否存在(存在其他線程修改的情況),單線程是可以的(不存在其他線程修改的情況)。
如果你確實需要在 ConcurrentHashMap 中使用 null 的話,可以使用一個特殊的靜態(tài)空對象來代替 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 tonullvs 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 能保證復合操作的原子性嗎?
ConcurrentHashMap 是線程安全的,意味著它可以保證多個線程同時對它進行讀寫操作時,不會出現(xiàn)數(shù)據(jù)不一致的情況,也不會導致 JDK1.7 及之前版本的 HashMap 多線程操作導致死循環(huán)問題。但是,這并不意味著它可以保證所有的復合操作都是原子性的,一定不要搞混了!
復合操作是指由多個基本操作(如put、get、remove、containsKey等)組成的操作,例如先判斷某個鍵是否存在containsKey(key),然后根據(jù)結果進行插入或更新put(key, value)。這種操作在執(zhí)行過程中可能會被其他線程打斷,導致結果不符合預期。
例如,有兩個線程 A 和 B 同時對 ConcurrentHashMap 進行復合操作,如下:
// 線程 A
if (!map.containsKey(key)) {
map.put(key, value);
}
// 線程 B
if (!map.containsKey(key)) {
map.put(key, anotherValue);
}
如果線程 A 和 B 的執(zhí)行順序是這樣:
-
線程 A 判斷 map 中不存在 key -
線程 B 判斷 map 中不存在 key -
線程 B 將 (key, anotherValue) 插入 map -
線程 A 將 (key, value) 插入 map
那么最終的結果是 (key, value),而不是預期的 (key, anotherValue)。這就是復合操作的非原子性導致的問題。
那如何保證 ConcurrentHashMap 復合操作的原子性呢?
ConcurrentHashMap 提供了一些原子性的復合操作,如 putIfAbsent、compute、computeIfAbsent 、computeIfPresent、merge等。這些方法都可以接受一個函數(shù)作為參數(shù),根據(jù)給定的 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 的時候,盡量使用這些原子性的復合操作方法來保證原子性。
推薦閱讀
更多關于 ConcurrentHashMap 大家可以自行去 JavaGuide 官方網站(javaguide.cn)上進行閱讀,內容非常全面。并且,上面提到的這兩個問題后續(xù)也會同步上去。
·············· END ··············
?? 近期文章精選:
