一、相關概念
1、Map的概念
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采用的是紅黑樹。
2、HashMap
實現了Map接口,實現了將唯一鍵隱射到特定值上。允許一個NULL鍵和多個NULL值。非線程安全。
3、HashTable
類似於HashMap,但是不允許NULL鍵和NULL值,比HashMap慢,因為它是同步的。HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨占。
4、ConcurrentHashMap
ConcurrentHashMap允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的Hashtable,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以並發進行。
JDK1.7的實現
在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成,如下圖所示:
Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每一個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構一樣
JDK1.8的實現
JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,並發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是為了兼容舊版本。
二、解決HashMap
的線程安全問題
有兩種方法可以解決HashMap
的線程安全問題:
- Java的
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
- 盡管沒有同步整個Map,但是它仍然是線程安全的
- 讀操作非常快,而寫操作則是通過加鎖完成的
- 在對象層次上不存在鎖(即不會阻塞線程)
- 鎖的粒度設置的非常好,只對哈希表的某一個key加鎖
ConcurrentHashMap
不會拋出ConcurrentModificationException
,即使一個線程在遍歷的同時,另一個線程嘗試進行修改。ConcurrentHashMap
會使用多個鎖
SynchronizedHashMap
- 會同步整個對象
- 每一次的讀寫操作都需要加鎖
- 對整個對象加鎖會極大降低性能
- 這相當於只允許同一時間內至多一個線程操作整個Map,而其他線程必須等待
- 它有可能造成資源沖突(某些線程等待較長時間)
SynchronizedHashMap
會返回Iterator
,當遍歷時進行修改會拋出異常
三、 Hashtable 和 HashMap
1、不同點
這兩個類主要有以下幾方面的不同:
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關鍵字進行了同步控制。
2. 潛在的線程安全問題
上面提到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 和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線程可以使用原來老的數據,而寫線程也可以並發的完成改變。