1. 程式人生 > >專用於高併發的map類-----Map的併發處理(ConcurrentHashMap)

專用於高併發的map類-----Map的併發處理(ConcurrentHashMap)

oncurrentModificationException

在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException, 取而代之的是在改變時new新的資料從而不影響原有的資料 ,iterator完成後再將頭指標替換為新的資料 ,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變。

ConcurrentHashMap 原理:

集合是程式設計中最常用的資料結構。而談到併發,幾乎總是離不開集合這類高階資料結構的支援。比如兩個執行緒需要同時訪問一箇中間臨界區 (Queue),比如常會用快取作為外部檔案的副本(HashMap)。這篇文章主要分析jdk1.5的3種併發集合型別 (concurrent,copyonright,queue)中的ConcurrentHashMap,讓我們從原理上細緻的瞭解它們,能夠讓我們在深 度專案開發中獲益非淺。

    在tiger之前,我們使用得最多的資料結構之一就是HashMap和Hashtable。大家都知道,HashMap中未進行同步考慮,而Hashtable則使用了synchronized,帶來的直接影響就是可選擇,我們可以在單執行緒時使用HashMap提高效率,而多執行緒時用Hashtable來保證安全。     當我們享受著jdk帶來的便利時同樣承受它帶來的不幸惡果。通過分析Hashtable就知道,synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔,安全的背後是巨大的浪費,慧眼獨具的Doug Lee立馬拿出瞭解決方案----ConcurrentHashMap。     ConcurrentHashMap和Hashtable主要區別就是圍繞著鎖的粒度以及如何鎖。如圖

    左邊便是Hashtable的實現方式---鎖整個hash表;而右邊則是ConcurrentHashMap的實現方式---鎖桶(或段)。 ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。試想,原來 只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,而讀執行緒幾乎不受限制,之後會提到),併發性的提升是顯而易見的。     更令人驚訝的是ConcurrentHashMap的讀取併發,因為在讀取的大多數時候都沒有用到鎖定,所以讀取操作幾乎是完全的併發操作,而寫操作鎖定的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現得更明顯些)。只有在求size等操作時才需要鎖定整個表。而在迭代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱為弱一致迭代器。在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的資料,iterator完成後再將頭指標替換為新的資料,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變,更重要的,這保證了多個執行緒併發執行的連續性和擴充套件性,是效能提升的關鍵。     接下來,讓我們看看ConcurrentHashMap中的幾個重要方法,心裡知道了實現機制後,使用起來就更加有底氣。     ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關係。     get方法(請注意,這裡分析的方法都是針對桶的,因為ConcurrentHashMap的最大改進就是將粒度細化到了桶上),首先判斷了當前桶的資料 個數是否為0,為0自然不可能get到什麼,只有返回null,這樣做避免了不必要的搜尋,也用最小的代價避免出錯。然後得到頭節點(方法將在下面涉及) 之後就是根據hash和key逐個判斷是否是指定的值,如果是並且值非空就說明找到了,直接返回;程式非常簡單,但有一個令人困惑的地方,這句 return readValueUnderLock(e)到底是用來幹什麼的呢?研究它的程式碼,在鎖定之後返回一個值。但這裡已經有一句V v = e.value得到了節點的值,這句return readValueUnderLock(e)是否多此一舉?事實上,這裡完全是為了併發考慮的,這裡當v為空時,可能是一個執行緒正在改變節點,而之前的get操作都未進行鎖定,根據bernstein條件,讀後寫或寫後讀都會引起資料的不一致,所以這裡要對這個e重新上鎖再讀一遍,以保證得到的是正確值,這裡不得不佩服Doug Lee思維的嚴密性。整個get操作只有很少的情況會鎖定,相對於之前的Hashtable,併發是不可避免的啊! Java程式碼  
收藏程式碼
  1. V get(Object key, int hash) {  
  2.     if (count != 0) { // read-volatile  
  3.         HashEntry e = getFirst(hash);  
  4.         while (e != null) {  
  5.             if (e.hash == hash && key.equals(e.key)) {  
  6.                 V v = e.value;  
  7.                 if (v != null)  
  8.                     return v;  
  9.                 return readValueUnderLock(e); // recheck  
  10.             }  
  11.             e = e.next;  
  12.         }  
  13.     }  
  14.     return null;  
  15. }  
  16. V readValueUnderLock(HashEntry e) {  
  17.     lock();  
  18.     try {  
  19.         return e.value;  
  20.     } finally {  
  21.         unlock();  
  22.     }  
  23. }  

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程式碼  收藏程式碼
  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.     lock();  
  3.     try {  
  4.         int c = count;  
  5.         if (c++ > threshold) // ensure capacity  
  6.             rehash();  
  7.         HashEntry[] tab = table;  
  8.         int index = hash & (tab.length - 1);  
  9.         HashEntry first = (HashEntry) tab[index];  
  10.         HashEntry e = first;  
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  12.             e = e.next;  
  13.         V oldValue;  
  14.         if (e != null) {  
  15.             oldValue = e.value;  
  16.             if (!onlyIfAbsent)  
  17.                 e.value = value;  
  18.         }  
  19.         else {  
  20.             oldValue = null;  
  21.             ++modCount;  
  22.             tab[index] = new HashEntry(key, hash, first, value);  
  23.             count = c; // write-volatile  
  24.         }  
  25.         return oldValue;  
  26.     } finally {  
  27.         unlock();  
  28.     }  
  29. }  

   remove操作非常類似put,但要注意一點區別,中間那個for迴圈是做什麼用的呢?(*號標記)從程式碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry 的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味著在第一次設定了next域之後便不能再 改變它,取而代之的是將它之前的節點全都克隆一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關,關於不變性的更多 內容,請參閱之前的文章《執行緒高階---執行緒的一些程式設計技巧》

Java程式碼  收藏程式碼
  1. V remove(Object key, int hash, Object value) {  
  2.     lock();  
  3.     try {  
  4.         int c = count - 1;  
  5.         HashEntry[] tab = table;  
  6.         int index = hash & (tab.length - 1);  
  7.         HashEntry first = (HashEntry)tab[index];  
  8.         HashEntry e = first;  
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  10.             e = e.next;  
  11.         V oldValue = null;  
  12.         if (e != null) {  
  13.             V v = e.value;  
  14.             if (value == null || value.equals(v)) {  
  15.                 oldValue = v;  
  16.                 // All entries following removed node can stay  
  17.                 // in list, but all preceding ones need to be  
  18.                 // cloned.  
  19.                 ++modCount;  
  20.                 HashEntry newFirst = e.next;  
  21.             *    for (HashEntry p = first; p != e; p = p.next)  
  22.             *        newFirst = new HashEntry(p.key, p.hash,   
  23.                                                   newFirst, p.value);  
  24.                 tab[index] = newFirst;  
  25.                 count = c; // write-volatile  
  26.             }  
  27.         }  
  28.         return oldValue;  
  29.     } finally {  
  30.         unlock();  
  31.     }  
  32. }  
Java程式碼  收藏程式碼
  1. static final class HashEntry {  
  2.     final K key;  
  3.     final int hash;  
  4.     volatile V value;  
  5.     final HashEntry next;  
  6.     HashEntry(K key, int hash, HashEntry next, V value) {  
  7.         this.key = key;  
  8.         this.hash = hash;  
  9.         this.next = next;  
  10.         this.value = value;  
  11.     }  
  12. }  

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

相關推薦

專用併發map-----Map併發處理ConcurrentHashMap

oncurrentModificationException 在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException, 取而代之的是在改變時new新的資料從而不影響原有的資料 ,iterator

Map併發處理ConcurrentHashMap

推薦相關文章: http://blog.csdn.net/waitforcher/archive/2009/05/24/4211896.aspx ConcurrentModificationException 在這種迭代方式中,當iterator被建立後集合再發

聊聊併發系統之限流特技(轉)

上一篇《聊聊高併發系統限流特技-1》講了限流演算法、應用級限流、分散式限流;本篇將介紹接入層限流實現。 接入層限流 接入層通常指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、快取、降級、限流、 A/B 測試、服務質量監控

《java併發程式設計實戰》:執行緒同步輔助之訊號量semaphore

1.訊號量的概念: 訊號量是一種計數器,用來保護一個或者多個共享資源的訪問,它是併發程式設計的一種基礎工具,大多數程式語言都提供了這個機制。 2、訊號量控制執行緒訪問流程: 如果執行緒要訪問一個共享資源,它必須先獲得訊號量。如果訊號量的內部計數器大於0,訊號量將減1,然後

微信小程式----map元件實現德地圖API實現wx.chooseLocationOBJECT

宣告 bug: 頁面搜尋返回的列表在真機測試是會出現不顯示問題? 造成原因:在小程式map元件的同一區域,map元件的檢視層比普通的文字檢視層要高,所以在真機會遮擋! 解決辦法:將該文字檢視採用c

多執行緒系列五:併發工具併發容器

一、併發容器 1.ConcurrentHashMap 為什麼要使用ConcurrentHashMap 在多執行緒環境下

《JavaScript級程序設計》讀書筆記基本概念第二小節 Number

一個 存在 ron 之前 value nbsp 瀏覽器 整數 操作 內容---語法 上一小節---數據類型 本小節 number類型---流程控制語句---理解函數 number類型--使用IEEE754格式來表示整數和浮點數值(雙精度數值)--規定了數值字面量格式,支

《JavaScript級程序設計》讀書筆記引用

ron 新增 訪問 -s 字符 版本 組織 返回 obj 內容---使用對象---創建並操作數組---理解基本的JavaScript類型---使用基本類型和基本包裝類型 引用類型--引用類型的值(對象)是引用類型的一個實例--在ECMAScript中,引用類型是一種數據結構

Map四種獲取key和value值的方法,以及對map中的元素排序

compareto map.entry 排序 區別 sta hashmap 得到 package log 獲取map的值主要有四種方法,這四種方法又分為兩類,一類是調用map.keySet()方法來獲取key和value的值,另一類則是通過map.entrySet()方法來

Golang併發原理及GPM排程策略

其實從一開始瞭解到go的goroutine概念就應該想到,其實go應該就是在核心級執行緒的基礎上做了一層邏輯上的虛擬執行緒(使用者級執行緒)+ 執行緒排程系統,如此分析以後,goroutine也就不再那麼神祕了。 併發≠並行 假如我們有一段CPU密集型任務,我們建立2000個gorountine是否真的可

mybatis Mapper 中resultType使用方法及返回值為Map的寫法 mybatis學習——resultType解析

  mybatis學習(七)——resultType解析 resultType是sql對映檔案中定義返回值型別,返回值有基本型別,物件型別,List型別,Map型別等。現總結一下再解釋 總結: resultType: 1、基本型別  :resultType=基本型別 2、Lis

java併發機制的底層實現原理:volatile深入分析

     java程式碼最終會被類載入器載入到JVM中,然後轉化為彙編指令在CPU上執行。java中所使用的併發機制依賴於JVM的實現和CPU的指令。 1.volatile的應用 volatile是一個輕量級的synchronize,它保證了共享變數的可見性,確保了所有執

16-Java併發程式設計:Timer和TimerTask轉載

Java併發程式設計:Timer和TimerTask(轉載)   下面內容轉載自:   其實就Timer來講就是一個排程器,而TimerTask呢只是一個實現了run方法的一個類,而具體的TimerTask需要由你自己來實現,例如這樣: Timer timer =

Java併發——執行緒安全的集合

1.對併發雜湊對映的批操作: Java SE 8為併發雜湊對映提供了批操作,即使有其他執行緒在處理對映,這些操作也能安全地執行。批操作會遍歷對映,處理遍歷過程中找到的元素。無須凍結當前對映的快照。 有三種不同的批操作:搜尋、歸約、forEach。 每個操作都有四個版本:operation

Java併發程式設計的藝術——讀書筆記 併發程式設計的挑戰

第一章 併發程式設計的挑戰 因為最近找工作,準備筆試/面試,開始嘗試閱讀這本書,我不常寫部落格,距上一次寫已經過去大概一年時間了,連CSDN密碼都忘了/衰,所以這次新開一個賬號重新開始,希望我能堅持下去。 第一章沒什麼內容,我認為其目的主要是給出足夠多的閱讀這本書的理

Java併發程式設計:Timer和TimerTask轉載

public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); }    另外兩個構造方法負責傳入名稱和將timer啟動: public Timer(String name, boo

java併發程式設計應用易忘知識點1java中的鎖

Lock介面 Java se5之後,出現了Lock介面,提供了與Synchronized類似同步功能。與synchronized相比,他雖然少了隱式獲取釋放鎖的便捷性,卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized不具備的同步特性

Java併發框架——AQS阻塞佇列管理——自旋鎖

我們知道一個執行緒在嘗試獲取鎖失敗後將被阻塞並加入等待佇列中,它是一個怎樣的佇列?又是如何管理此佇列?這節聊聊CHL Node FIFO佇列。 在談到CHL Node FIFO佇列之前,我們先分析這種佇列的幾個要素。首先要了解的是自旋鎖,所謂自旋鎖即是某一執行緒去嘗試獲取某個

java併發-原子性和可見性7

原子性:某個操作同時只能由一個執行緒執行。 可見性:一個執行緒的修改對其他執行緒是可見的。也就是A執行緒修改了變數x,那麼B,C,D...執行緒此時去拿到的x肯定是A修改之後的值。 package com.concurenny.chapter.six; /** * 建

JAVA併發程式設計——執行緒協作通訊

執行緒間的協作 在前面我們瞭解了很多關於同步(互斥鎖)的問題,下面來看一下執行緒之間的協作。這裡主要說一下Java執行緒中的join()、sleep()、yield()、wait()、notify()和notifyAll()方法。其中wait()、notify(