在開始以前,先介紹下Map是什麼?java
javadoc中對Map的解釋以下:安全
An objectthat maps keys to values . Amap cannot contain duplicatekeys; each key can map to at most one value.
This interface takes the place of the Dictionary class, which was atotally abstract class rather than an interface.
The Map interface provides three collection views, which allow amap's contents to be viewed as a set of keys, collection of values,or set of key-value mappings.多線程
從上可知,Map用於存儲「key-value」元素對,它將一個key映射到一個並且只能是惟一的一個value。併發
Map可使用多種實現方式,HashMap的實現採用的是hash表;而TreeMap採用的是紅黑樹。app
1. Hashtable 和 HashMapide
這兩個類主要有如下幾方面的不一樣:高併發
Hashtable和HashMap都實現了Map接口,可是Hashtable的實現是基於Dictionary抽象類。性能
在HashMap中,null能夠做爲鍵,這樣的鍵只有一個;能夠有一個或多個鍵所對應的值爲null。當get()方法返回null值時,便可以表示HashMap中沒有該鍵,也能夠表示該鍵所對應的值爲null。所以,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。而在Hashtable中,不管是key仍是value都不能爲null。spa
這兩個類最大的不一樣在於Hashtable是線程安全的,它的方法是同步了的,能夠直接用 在多線程環境中。而HashMap則不是線程安全的。在多線程環境中,須要手動實現同步機制。所以,在Collections類中提供了一個方法返回一個 同步版本的HashMap用於多線程的環境:線程
Java代碼
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<K,V>(m); }
該方法返回的是一個SynchronizedMap的實例。SynchronizedMap類是定義在Collections中的一個靜態內部類。它實現了Map接口,並對其中的每個方法實現,經過synchronized關鍵字進行了同步控制。
2. 潛在的線程安全問題
上面提到Collections爲HashMap提供了一個併發版本SynchronizedMap。這個版本中的方法都進行了同步,可是這並不等於這個類就必定是線程安全的。在某些時候會出現一些意想不到的結果。
以下面這段代碼:
Java代碼
// shm是SynchronizedMap的一個實例 if(shm.containsKey('key')){ shm.remove(key); }
這段代碼用於從map中刪除一個元素以前判斷是否存在這個元素。這裏的 containsKey和reomve方法都是同步的,可是整段代碼卻不是。考慮這麼一個使用場景:線程A執行了containsKey方法返回 true,準備執行remove操做;這時另外一個線程B開始執行,一樣執行了containsKey方法返回true,並接着執行了remove操做;然 後線程A接着執行remove操做時發現此時已經沒有這個元素了。要保證這段代碼按咱們的意願工做,一個辦法就是對這段代碼進行同步控制,可是這麼作付出 的代價太大。
在進行迭代時這個問題更改明顯。Map集合共提供了三種方式來分別返回鍵、值、鍵值對的集合:
Java代碼
Set<K> keySet(); Collection<V> values(); Set<Map.Entry<K,V>> entrySet();
在這三個方法的基礎上,咱們通常經過以下方式訪問Map的元素:
Java代碼
Iterator keys = map.keySet().iterator(); while(keys.hasNext()){ map.get(keys.next()); }
在這裏,有一個地方須要注意的是:獲得的keySet和迭代器都是Map中元素的一個「視圖」,而不是「副本」。問題也就出如今這裏,當一個線程正在迭代Map中的元素時,另外一個線程可能正在修改其中的元素。此時,在迭代元素時就可能會拋出ConcurrentModificationException異常。爲了解決這個問題一般有兩種方法,一是直接返回元素的副本,而不是視圖。這個能夠經過
集合類的 toArray()方法實現,可是建立副本的方式效率比以前有所下降,特別是在元素不少的狀況下;另外一種方法就是在迭代的時候鎖住整個集合,這樣的話效率就更低了。
3. 更好的選擇:ConcurrentHashMap
效率低下的HashTable容器
HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的狀況 下HashTable的效率很是低下。由於當一個線程訪問HashTable的同步方法時,其餘線程訪問HashTable的同步方法時,可能會進入阻塞 或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,而且也不能使用get方法來獲取元素,因此競爭越激烈效率越低。
鎖分段技術
HashTable容器在競爭激烈的併發環境下表現出效率低下的緣由是全部訪問HashTable的線程都必須競爭同一把鎖,那假如容器裏有多把 鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效的提升併發訪問效率,這就是 ConcurrentHashMap所使用的鎖分段技術,首先將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據 的時候,其餘段的數據也能被其餘線程訪問。
java5中新增了ConcurrentMap接口和它的一個實現類 ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不一樣的鎖機 制。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而同一時刻只能由一個線程對其進行操做;而ConcurrentHashMap中則是 一次鎖住一個桶。ConcurrentHashMap默認將hash表分爲16個桶,諸如get,put,remove等經常使用操做只鎖當前須要用到的桶。 這樣,原來只能一個線程進入,如今卻能同時有16個寫線程執行,併發性能的提高是顯而易見的。
上面說到的16個線程指的是寫線程,而讀操做大部分時候都不須要用到鎖。只有在size等操做時才須要鎖住整個hash表。
在迭代方面,ConcurrentHashMap使用了一種不一樣的迭代方式。在這種迭代方式中,當iterator被建立後集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據,iterator完成後再將頭指針替換爲新的數據,這樣iterator線程可使用原來老的數據,而寫線程也能夠併發的完成改變。