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

週末的時候,有一位小夥伴提了一些關於 ConcurrentHashMap 的問題,都是他最近面試遇到的。原提問如下:

整個提問看着非常複雜,其實歸納來說就是兩個問題:

  1. ConcurrentHashMap 爲什麼 key 和 value 不能爲 null?
  2. ConcurrentHashMap 能保證複合操作的原子性嗎?

下面我會以此提供這兩個問題的詳細答案,希望對你有幫助。

ConcurrentHashMap 爲什麼 key 和 value 不能爲 null?

ConcurrentHashMap 的 key 和 value 不能爲 null 主要是爲了避免二義性。null 是一個特殊的值,表示沒有對象或沒有引用。如果你用 null 作爲鍵,那麼你就無法區分這個鍵是否存在於 ConcurrentHashMap 中,還是根本沒有這個鍵。同樣,如果你用 null 作爲值,那麼你就無法區分這個值是否是真正存儲在 ConcurrentHashMap 中的,還是因爲找不到對應的鍵而返回的。

拿 get 方法取值來說,返回的結果爲 null 存在兩種情況:

  • 值沒有在集合中 ;
  • 值本身就是 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) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

翻譯過來之後的,大致意思還是單線程下可以容忍歧義,而多線程下無法容忍。

ConcurrentHashMap 能保證複合操作的原子性嗎?

ConcurrentHashMap 是線程安全的,意味着它可以保證多個線程同時對它進行讀寫操作時,不會出現數據不一致的情況,也不會導致 JDK1.7 及之前版本的 HashMap 多線程操作導致死循環問題。但是,這並不意味着它可以保證所有的複合操作都是原子性的,一定不要搞混了!

複合操作是指由多個基本操作(如putgetremovecontainsKey等)組成的操作,例如先判斷某個鍵是否存在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 的執行順序是這樣:

  1. 線程 A 判斷 map 中不存在 key
  2. 線程 B 判斷 map 中不存在 key
  3. 線程 B 將 (key, anotherValue) 插入 map
  4. 線程 A 將 (key, value) 插入 map

那麼最終的結果是 (key, value),而不是預期的 (key, anotherValue)。這就是複合操作的非原子性導致的問題。

那如何保證 ConcurrentHashMap 複合操作的原子性呢?

ConcurrentHashMap 提供了一些原子性的複合操作,如 putIfAbsentcomputecomputeIfAbsentcomputeIfPresentmerge等。這些方法都可以接受一個函數作爲參數,根據給定的 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 的時候,儘量使用這些原子性的複合操作方法來保證原子性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章