詳見:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt215
SynchronizedMap和ConcurrentHashMap的深入分析
在開始之前,先介紹下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采用的是紅黑樹。
1. Hashtable 和 HashMap
這兩個類主要有以下幾方面的不同:
Hashtable和HashMap都實現了Map接口,但是Hashtable的實現是基於Dictionary抽象類。
在HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。而在Hashtable中,無論是key還是value都不能為null。
這兩個類最大的不同在於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線程可以使用原來老的數據,而寫線程也可以並發的完成改變。