javadoc中對Map的解釋以下:java
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採用的是紅黑樹。數據結構
實現了Map接口,實現了將惟一鍵隱射到特定值上。容許一個NULL鍵和多個NULL值。非線程安全。多線程
相似於HashMap,可是不容許NULL鍵和NULL值,比HashMap慢,由於它是同步的。HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨佔。併發
ConcurrentHashMap容許多個修改操做併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不一樣部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不一樣的部分,每一個段其實就是一個小的Hashtable,它們有本身的鎖。只要多個修改操做發生在不一樣的段上,它們就能夠併發進行。app
在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成,以下圖所示:ide
Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構同樣高併發
JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操做,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本。性能
HashMap
的線程安全問題有兩種方法能夠解決HashMap
的線程安全問題:
Collections
庫中的synchronizedMap()
方法ConcurrentHashMap
譯者注:其實還有第三種方法,使用Hashtable
。不過Hashtable
是Java 1.1提供的舊有類,從性能上和使用上都不如其餘的替代類,所以已經不推薦使用
//Hashtable
Map<String, String> normalMap = new Hashtable<String, String>();
//synchronizedMap
synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
concurrentHashMap = new ConcurrentHashMap<String, String>();
ConcurrentHashMap
ConcurrentHashMap
不會拋出ConcurrentModificationException
,即便一個線程在遍歷的同時,另外一個線程嘗試進行修改。ConcurrentHashMap
會使用多個鎖SynchronizedHashMap
會返回Iterator
,當遍歷時進行修改會拋出異常
這兩個類主要有如下幾方面的不一樣:
Hashtable和HashMap都實現了Map接口,可是Hashtable的實現是基於Dictionary抽象類。
在HashMap中,null能夠做爲鍵,這樣的鍵只有一個;能夠有一個或多個鍵所對應的值爲null。當get()方法返回null值時,便可以表示HashMap中沒有該鍵,也能夠表示該鍵所對應的值爲null。所以,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。而在Hashtable中,不管是key仍是value都不能爲null。
這兩個類最大的不一樣在於Hashtable是線程安全的,它的方法是同步了的,能夠直接用 在多線程環境中。而HashMap則不是線程安全的。在多線程環境中,須要手動實現同步機制。所以,在Collections類中提供了一個方法返回一個 同步版本的HashMap用於多線程的環境:
1 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { 2 return new SynchronizedMap<K,V>(m); 3 }
該方法返回的是一個SynchronizedMap的實例。SynchronizedMap類是定義在Collections中的一個靜態內部類。它實現了Map接口,並對其中的每個方法實現,經過synchronized關鍵字進行了同步控制。
上面提到Collections爲HashMap提供了一個併發版本SynchronizedMap。這個版本中的方法都進行了同步,可是這並不等於這個類就必定是線程安全的。在某些時候會出現一些意想不到的結果。
以下面這段代碼:
1 // shm是SynchronizedMap的一個實例
2 if(shm.containsKey('key')){ 3 shm.remove(key); 4 }
這段代碼用於從map中刪除一個元素以前判斷是否存在這個元素。這裏的 containsKey和reomve方法都是同步的,可是整段代碼卻不是。考慮這麼一個使用場景:線程A執行了containsKey方法返回 true,準備執行remove操做;這時另外一個線程B開始執行,一樣執行了containsKey方法返回true,並接着執行了remove操做;然 後線程A接着執行remove操做時發現此時已經沒有這個元素了。要保證這段代碼按咱們的意願工做,一個辦法就是對這段代碼進行同步控制,可是這麼作付出 的代價太大。
在進行迭代時這個問題更改明顯。Map集合共提供了三種方式來分別返回鍵、值、鍵值對的集合:
Set<K> keySet(); Collection<V> values(); Set<Map.Entry<K,V>> entrySet();
在這三個方法的基礎上,咱們通常經過以下方式訪問Map的元素:
1 Iterator keys = map.keySet().iterator(); 2
3 while(keys.hasNext()){ 4 map.get(keys.next()); 5 }
在這裏,有一個地方須要注意的是:獲得的keySet和迭代器都是Map中元素的一個「視圖」,而不是「副本」。問題也就出如今這裏,當一個線程正在迭代Map中的元素時,另外一個線程可能正在修改其中的元素。此時,在迭代元素時就可能會拋出ConcurrentModificationException異常。爲了解決這個問題一般有兩種方法,一是直接返回元素的副本,而不是視圖。這個能夠經過
集合類的 toArray()方法實現,可是建立副本的方式效率比以前有所下降,特別是在元素不少的狀況下;另外一種方法就是在迭代的時候鎖住整個集合,這樣的話效率就更低了。
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線程可使用原來老的數據,而寫線程也能夠併發的完成改變。