Map的併發處理(ConcurrentHashMap)
推薦相關文章:
http://blog.csdn.net/waitforcher/archive/2009/05/24/4211896.aspx
ConcurrentModificationException
在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException, 取而代之的是在改變時new新的資料從而不影響原有的資料 ,iterator完成後再將頭指標替換為新的資料 ,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變。
ConcurrentHashMap 原理:
集合是程式設計中最常用的資料結構。而談到併發,幾乎總是離不開集合這類高階資料結構的支援。比如兩個執行緒需要同時訪問一箇中間臨界區 (Queue),比如常會用快取作為外部檔案的副本(HashMap)。這篇文章主要分析jdk1.5的3種併發集合型別 (concurrent,copyonright,queue)中的ConcurrentHashMap,讓我們從原理上細緻的瞭解它們,能夠讓我們在深 度專案開發中獲益非淺。
- V get(Object key, int hash) {
- if (count != 0) { // read-volatile
- HashEntry e = getFirst(hash);
- while (e != null) {
- if (e.hash == hash && key.equals(e.key)) {
- V v = e.value;
- if (v != null)
- return v;
- return readValueUnderLock(e); // recheck
- }
- e = e.next;
- }
- }
- return null;
- }
- V readValueUnderLock(HashEntry e) {
- lock();
- try {
- return e.value;
- } finally {
- unlock();
- }
- }
put操作一上來就鎖定了整個segment,這當然是為了併發的安全,修改資料是不能併發進行的,必須得有個判斷是否超限的語句以確保容量不足時能夠rehash,而比較難懂的是這句int index = hash & (tab.length - 1),原來segment裡面才是真正的hashtable,即每個segment是一個傳統意義上的hashtable,如上圖,從兩者的結構就可以看出區別,這裡就是找出需要的entry在table的哪一個位置,之後得到的entry就是這個鏈的第一個節點,如果e!=null,說明找到了,這是就要替換節點的值(onlyIfAbsent == false),否則,我們需要new一個entry,它的後繼是first,而讓tab[index]指向它,什麼意思呢?實際上就是將這個新entry插入到鏈頭,剩下的就非常容易理解了。
Java程式碼- V put(K key, int hash, V value, boolean onlyIfAbsent) {
- lock();
- try {
- int c = count;
- if (c++ > threshold) // ensure capacity
- rehash();
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry) tab[index];
- HashEntry e = first;
- while (e != null && (e.hash != hash || !key.equals(e.key)))
- e = e.next;
- V oldValue;
- if (e != null) {
- oldValue = e.value;
- if (!onlyIfAbsent)
- e.value = value;
- }
- else {
- oldValue = null;
- ++modCount;
- tab[index] = new HashEntry(key, hash, first, value);
- count = c; // write-volatile
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
remove操作非常類似put,但要注意一點區別,中間那個for迴圈是做什麼用的呢?(*號標記)從程式碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry 的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味著在第一次設定了next域之後便不能再 改變它,取而代之的是將它之前的節點全都克隆一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關,關於不變性的更多 內容,請參閱之前的文章《執行緒高階---執行緒的一些程式設計技巧》
Java程式碼- V remove(Object key, int hash, Object value) {
- lock();
- try {
- int c = count - 1;
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry)tab[index];
- HashEntry e = first;
- while (e != null && (e.hash != hash || !key.equals(e.key)))
- e = e.next;
- V oldValue = null;
- if (e != null) {
- V v = e.value;
- if (value == null || value.equals(v)) {
- oldValue = v;
- // All entries following removed node can stay
- // in list, but all preceding ones need to be
- // cloned.
- ++modCount;
- HashEntry newFirst = e.next;
- * for (HashEntry p = first; p != e; p = p.next)
- * newFirst = new HashEntry(p.key, p.hash,
- newFirst, p.value);
- tab[index] = newFirst;
- count = c; // write-volatile
- }
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
- static final class HashEntry {
- final K key;
- final int hash;
- volatile V value;
- final HashEntry next;
- HashEntry(K key, int hash, HashEntry next, V value) {
- this.key = key;
- this.hash = hash;
- this.next = next;
- this.value = value;
- }
- }
util.concurrent
包中的 ConcurrentHashMap
類(也將出現在JDK 1.5中的 java.util.concurrent
包中)是對 Map
的執行緒安全的實現,比起 synchronizedMap
來,它提供了好得多的併發性。多個讀操作幾乎總可以併發地執行,同時進行的讀和寫操作通常也能併發地執行,而同時進行的寫操作仍然可以不時地併發進行(相關的類也提供了類似的多個讀執行緒的併發性,但是,只允許有一個活動的寫執行緒) 。ConcurrentHashMap
被設計用來優化檢索操作;實際上,成功的 get()
操作完成之後通常根本不會有鎖著的資源。要在不使用鎖的情況下取得執行緒安全性需要一定的技巧性,並且需要對Java記憶體模型(Java
Memory Model)的細節有深入的理解。ConcurrentHashMap
實現,加上 util.concurrent
包的其他部分,已經被研究正確性和執行緒安全性的併發專家所正視。在下個月的文章中,我們將看看 ConcurrentHashMap
的實現的細節。
ConcurrentHashMap
通過稍微地鬆弛它對呼叫者的承諾而獲得了更高的併發性。檢索操作將可以返回由最近完成的插入操作所插入的值,也可以返回在步調上是併發的插入操作所新增的值(但是決不會返回一個沒有意義的結果)。由 ConcurrentHashMap.iterator()
返回的 Iterators
將每次最多返回一個元素,並且決不會丟擲ConcurrentModificationException
異常,但是可能會也可能不會反映在該迭代器被構建之後發生的插入操作或者移除操作。在對
集合進行迭代時,不需要表範圍的鎖就能提供執行緒安全性。在任何不依賴於鎖整個表來防止更新的應用程式中,可以使用 ConcurrentHashMap
來替代 synchronizedMap
或 Hashtable
。
上述改進使得 ConcurrentHashMap
能夠提供比 Hashtable
高得多的可伸縮性,而且,對於很多型別的公用案例(比如共享的cache)來說,還不用損失其效率。
表 1對 Hashtable
和 ConcurrentHashMap
的可伸縮性進行了粗略的比較。在每次執行過程中, n 個執行緒併發地執行一個死迴圈,在這個死迴圈中這些執行緒從一個 Hashtable
或者 ConcurrentHashMap
中檢索隨機的key
value,發現在執行 put()
操作時有80%的檢索失敗率,在執行操作時有1%的檢索成功率。測試所在的平臺是一個雙處理器的Xeon系統,作業系統是Linux。資料顯示了10,000,000次迭代以毫秒計的執行時間,這個資料是在將對 ConcurrentHashMap的
操作標準化為一個執行緒的情況下進行統計的。您可以看到,當執行緒增加到多個時,ConcurrentHashMap
的效能仍然保持上升趨勢,而 Hashtable
的效能則隨著爭用鎖的情況的出現而立即降了下來。
比起通常情況下的伺服器應用,這次測試中執行緒的數量看上去有點少。然而,因為每個執行緒都在不停地對錶進行操作,所以這與實際環境下使用這個表的更多數量的執行緒的爭用情況基本等同。
執行緒數 | ConcurrentHashMap | Hashtable |
1 | 1.00 | 1.03 |
2 | 2.59 | 32.40 |
4 | 5.58 | 78.23 |
8 | 13.21 | 163.48 |
16 | 27.58 | 341.21 |
32 | 57.27 | 778.41 |