天天看點

關于ConcurrentHashMap的key和value不能為null的深層次原因

前面分析ConcurrentHashMap的過程中可以發現,其要求key和value不能為空。實際上,不僅僅是ConcurrentHashMap,前面的HashTable,以及ConcurrentSkipListMap,這些并發的Map都不允許為空。在面試的過程中,不少大廠也會拿這個問題做為追問的問題之一。那麼我們就來具體聊聊為什麼不能為null的深層次的原因。

層次1:源碼不支援

是的,實際上确實是在源碼上就沒用提供支援。在hashtable中,對value進行了非空校驗,而key,如果為空則會出現NullPointerException。

if (value == null) {
    throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();           

複制

key如果為空,則hashCode方法會出現空指針異常。

在ConcurrentHashMap中,和ConcurrentSkipListMap中,則分别進行了非空限制。

ConcurrentHashMap:

if (key == null || value == null) throw new NullPointerException();           

複制

ConcurrentSkipListMap:

public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    return doPut(key, value, false);
}           

複制

而這個doPut方法中:

if (key == null)
    throw new NullPointerException();           

複制

從上面可以看出,在代碼中直接就杜絕了使用null的可能性,隻有HashMap是支援null的,但是是在put為空的時候,hash方法對null做了特殊處理,為null的時候hash值位0。

層次2:null會帶來二義性

之是以并發的ConcurrentHashMap不支援null的深層次的原因在于,null會帶來難以容忍的二義性。我們可以看看Doug Lea對這個問題的描述。

Handling Null Values in ConcurrentHashMap

Tutika Chakravarthy wrote:
> Hi ,
> I would like to replace some Hashmaps in our
> application, which are prone to multi threading issues
> with ConCurrentHashMap.
> 
> Currently we keep null key and values in hashmap
> without any issues as HashMap allows them.
> 
> But ConcurrentHashMap does not allow any null key and
> values .
> 

Try to take Holger's advice. As mostly an aside though...

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.

Further digressing: I personally think that allowing
nulls in Maps (also Sets) is an open invitation for programs
to contain errors that remain undetected until
they break at just the wrong time. (Whether to allow nulls even
in non-concurrent Maps/Sets is one of the few design issues surrounding
Collections that Josh Bloch and I have long disagreed about.)

> 
> It is very difficult to check for null keys and values
> in my entire application .
> 

Would it be easier to declare somewhere
   static final Object NULL = new Object();
and replace all use of nulls in uses of maps with NULL?

-Doug           

複制

原作者認為,在ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps)上,不允許null值的出現的主要原因是他可能會在并發的情況下帶來難以容忍的二義性。如果在HashMap等非并發容器中,你可以通過contains方法來判斷,這個key是究竟不存在,還是本來就是null。但是在并發容器中,如果允許空值的存在的話,你就沒法判斷真正的情況。用作者的話說就是:在Maps或者Sets集合中允許null值的存在,就是公開邀請錯誤進入你的程式。而這些錯誤,隻有在發生錯誤的情況下才能被發現。

我們可以對HashMap進行測試:

public static void main(String[] args) {
	HashMap map = new HashMap();
	map.put(null,1);
	System.out.println(map.containsKey(null));
	map.put(1,null);
	System.out.println(map.containsValue(null));
	System.out.println(map.get(null));
}           

複制

此時輸出:

true
true
1           

複制

可見,在HashMap之中,我們可以很容易的通過contains方法來判斷key或者value為null是否真的存在。

但是這個問題要是出現在ConurrentMaps中了,那麼就可能會有問題了。試想一下,當我們首先從map中get某個key,由于map中這個key不存在,那麼會傳回null,這之後我們通過contains進行判斷,此時如果有線程并發寫入了一條value為null的值,那麼contains的結果就為true。這樣就會與真實的情況不一緻了,這就是二義性。

是以我們也需要注意Doug 的觀點:不管容器是否考慮了線程安全問題,都不應該允許null值的出現。他覺得在現有的某些集合裡面允許了null值的出現,是集合的設計問題。