java中ConcurrentHashMap的使用及在Java 8中的衝突方案

java中ConcurrentHashMap的使用及在Java 8中的衝突方案

一、引言

ConcurrentHashMap(簡稱CHM)是在Java 1.5做爲Hashtable的替代選擇新引入的,是concurrent包的重要成員。在Java 1.5以前,若是想要實現一個能夠在多線程和併發的程序中安全使用的Map,只能在HashTable和synchronized Map中選擇,由於HashMap並非線程安全的。但再引入了CHM以後,咱們有了更好的選擇。CHM不可是線程安全的,並且比HashTable和synchronizedMap的性能要好。相對於HashTable和synchronizedMap鎖住了整個Map,CHM只鎖住部分Map。CHM容許併發的讀操做,同時經過同步鎖在寫操做時保持數據完整性。在這篇博客中我將介紹如下幾點:html

  • CHM在Java中如何實現的java

  • 什麼狀況下應該使用CHM數組

  • 在Java中使用CHM的例子安全

  • CHM的一些重要特性多線程

二、Java中ConcurrentHashMap的實現

CHM引入了分割,並提供了HashTable支持的全部的功能。在CHM中,支持多線程對Map作讀操做,而且不須要任何的blocking。這得益於CHM將Map分割成了不一樣的部分,在執行更新操做時只鎖住一部分。根據默認的併發級別(concurrency level),Map被分割成16個部分,而且由不一樣的鎖控制。這意味着,同時最多能夠有16個寫線程操做Map。試想一下,由只能一個線程進入變成同時可由16個寫線程同時進入(讀線程幾乎不受限制),性能的提高是顯而易見的。但因爲一些更新操做,如put(),remove(),putAll(),clear()只鎖住操做的部分,因此在檢索操做不能保證返回的是最新的結果。併發

另外一個重要點是在迭代遍歷CHM時,keySet返回的iterator是弱一致和fail-safe的,可能不會返回某些最近的改變,而且在遍歷過程當中,若是已經遍歷的數組上的內容變化了,不會拋出ConcurrentModificationExceptoin的異常。函數

CHM默認的併發級別是16,但能夠在建立CHM時經過構造函數改變。毫無疑問,併發級別表明着併發執行更新操做的數目,因此若是隻有不多的線程會更新Map,那麼建議設置一個低的併發級別。另外,CHM還使用了ReentrantLock來對segments加鎖。性能

三、Java中ConcurrentHashMap putifAbsent方法的例子

不少時候咱們但願在元素不存在時插入元素,咱們通常會像下面那樣寫代碼優化

synchronized(map){
  if (map.get(key) == null){
  return map.put(key, value);
  } else{
  return map.get(key);
  }
}

上面這段代碼在HashMap和HashTable中是好用的,但在CHM中是有出錯的風險的。這是由於CHM在put操做時並無對整個Map加鎖,因此一個線程正在put(k,v)的時候,另外一個線程調用get(k)會獲得null,這就會形成一個線程put的值會被另外一個線程put的值所覆蓋。固然,你能夠將代碼封裝到synchronized代碼塊中,這樣雖然線程安全了,但會使你的代碼變成了單線程。CHM提供的putIfAbsent(key,value)方法原子性的實現了一樣的功能,同時避免了上面的線程競爭的風險。線程

四、何時使用ConcurrentHashMap

CHM適用於讀者數量超過寫者時,當寫者數量大於等於讀者時,CHM的性能是低於Hashtable和synchronized Map的。這是由於當鎖住了整個Map時,讀操做要等待對同一部分執行寫操做的線程結束。CHM適用於作cache,在程序啓動時初始化,以後能夠被多個請求線程訪問。正如Javadoc說明的那樣,CHM是HashTable一個很好的替代,但要記住,CHM的比HashTable的同步性稍弱。

五、使用小結

如今咱們知道了什麼是ConcurrentHashMap和何時該用ConcurrentHashMap,下面咱們來複習一下CHM的一些關鍵點。

  • CHM容許併發的讀和線程安全的更新操做

  • 在執行寫操做時,CHM只鎖住部分的Map

  • 併發的更新是經過內部根據併發級別將Map分割成小部分實現的

  • 高的併發級別會形成時間和空間的浪費,低的併發級別在寫線程多時會引發線程間的競爭

  • CHM的全部操做都是線程安全

  • CHM返回的迭代器是弱一致性,fail-safe而且不會拋出ConcurrentModificationException異常

  • CHM不容許null的鍵值

  • 可使用CHM代替HashTable,但要記住CHM不會鎖住整個Map

以上就是Java中CHM的實現和使用場景,下面作進一步深刻探究。

六、衝突解決方案

在Java 8 以前,HashMap和其餘基於map的類都是經過鏈地址法解決衝突,它們使用單向鏈表來存儲相同索引值的元素。在最壞的狀況下,這種方式會將HashMap的get方法的性能從O(1)下降到O(n)。爲了解決在頻繁衝突時hashmap性能下降的問題,Java 8中使用平衡樹來替代鏈表存儲衝突的元素。這意味着咱們能夠將最壞狀況下的性能從O(n)提升到O(logn)

在Java 8中使用常量TREEIFY_THRESHOLD來控制是否切換到平衡樹來存儲。目前,這個常量值是8,這意味着當有超過8個元素的索引同樣時,HashMap會使用樹來存儲它們。

這一改變是爲了繼續優化經常使用類。你們可能還記得在Java 7中爲了優化經常使用類對ArrayList和HashMap採用了延遲加載的機制,在有元素加入以前不會分配內存,這會減小空的鏈表和HashMap佔用的內存。
這一動態的特性使得HashMap一開始使用鏈表,並在衝突的元素數量超過指定值時用平衡二叉樹替換鏈表。不過這一特性在全部基於hash table的類中並無,例如Hashtable和WeakHashMap。

目前,只有ConcurrentHashMap,LinkedHashMap和HashMap會在頻繁衝突的狀況下使用平衡樹。

七、何時會產生衝突

HashMap中調用hashCode()方法來計算hashCode。
因爲在Java中兩個不一樣的對象可能有同樣的hashCode,因此不一樣的鍵可能有同樣hashCode,從而致使衝突的產生。

八、衝突解決小結

  • HashMap在處理衝突時使用鏈表存儲相同索引的元素。

  • 從Java 8開始,HashMap,ConcurrentHashMap和LinkedHashMap在處理頻繁衝突時將使用平衡樹來代替鏈表,當同一hash桶中的元素數量超過特定的值便會由鏈表切換到平衡樹,這會將get()方法的性能從O(n)提升到O(logn)。

  • 當從鏈表切換到平衡樹時,HashMap迭代的順序將會改變。不過這並不會形成什麼問題,由於HashMap並無對迭代的順序提供任何保證。

  • 從Java 1中就存在的Hashtable類爲了保證迭代順序不變,即使在頻繁衝突的狀況下也不會使用平衡樹。這一決定是爲了避免破壞某些較老的須要依賴於Hashtable迭代順序的Java應用。

  • 除了Hashtable以外,WeakHashMap和IdentityHashMap也不會在頻繁衝突的狀況下使用平衡樹。

  • 使用HashMap之因此會產生衝突是由於使用了鍵對象的hashCode()方法,而equals()和hashCode()方法不保證不一樣對象的hashCode是不一樣的。須要記住的是,相同對象的hashCode必定是相同的,但相同的hashCode不必定是相同的對象。

  • 在HashTable和HashMap中,衝突的產生是因爲不一樣對象的hashCode()方法返回了同樣的值。
    以上就是Java中HashMap如何處理衝突。這種方法被稱爲鏈地址法,由於使用鏈表存儲同一桶內的元素。一般狀況HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均採用這種方法處理衝突。

從JDK 8開始,HashMap,LinkedHashMap和ConcurrentHashMap爲了提高性能,在頻繁衝突的時候使用平衡樹來替代鏈表。由於HashSet內部使用了HashMap,LinkedHashSet內部使用了LinkedHashMap,因此他們的性能也會獲得提高。

http://javarevisited.blogspot.com/2013/02/concurrenthashmap-in-java-example-tutorial-working.html
http://javarevisited.blogspot.jp/2016/01/how-does-java-hashmap-or-linkedhahsmap-handles.html

相關文章
相關標籤/搜索