Java容器:HashTable, synchronizedMap與ConcurrentHashMap

首先須要明確的是,無論使用那種Map,都不能保證公共混合調用的線程安全,只能保證單條操做的線程安全,在這一點上各Map不存在優劣。html

前文中簡單說過HashTable和synchronizedMap,其實這兩個類不須要說太多,把代碼貼一下相信看過Java多線程的就能很容易理解了。java

HashTable

HashTable的話,實現這個樣子的。能夠看到的是,對於Hash表的全部操做,HashTable都加了鎖,但也只能保證單條操做的線程安全。數組

public synchronized V get(Object key) {
       // 省略實現
}
public synchronized V put(K key, V value) {
    // 省略實現
}

synchronizedMap

synchronizedMap的實現以下,沒直接在方法上加,儘管其實質與HashTable是等效的,也一樣有HashTable的缺陷,但synchronizedMap給用戶留下了選擇的空間:用戶能夠在不須要加鎖時直接操做原始Map,在實際編碼時就能夠基於這點進行優化。安全

// synchronizedMap方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
       return new SynchronizedMap<>(m);
   }
// SynchronizedMap類
private static class SynchronizedMap<K,V>
       implements Map<K,V>, Serializable {
       private static final long serialVersionUID = 1978198479659022715L;

       private final Map<K,V> m;     // Backing Map
       final Object      mutex;        // Object on which to synchronize

       SynchronizedMap(Map<K,V> m) {
           this.m = Objects.requireNonNull(m);
           mutex = this;
       }

       SynchronizedMap(Map<K,V> m, Object mutex) {
           this.m = m;
           this.mutex = mutex;
       }

       public int size() {
           synchronized (mutex) {return m.size();}
       }
       public boolean isEmpty() {
           synchronized (mutex) {return m.isEmpty();}
       }
       public boolean containsKey(Object key) {
           synchronized (mutex) {return m.containsKey(key);}
       }
       public boolean containsValue(Object value) {
           synchronized (mutex) {return m.containsValue(value);}
       }
       public V get(Object key) {
           synchronized (mutex) {return m.get(key);}
       }

       public V put(K key, V value) {
           synchronized (mutex) {return m.put(key, value);}
       }
       public V remove(Object key) {
           synchronized (mutex) {return m.remove(key);}
       }
       // 省略其餘方法
}

ConcurrentHashMap

提升安全HashMap的併發性的方法,能夠經過減少鎖粒度的方式,不對整個Hash表加鎖,而是對每一個bucket加鎖,甚至用鎖池,每一個鎖維護幾個bucket,讓Map的不一樣部分能夠被多個線程訪問,不過這樣的方式會讓對總體集合操做的方法的實現更加困難。Java7中的ConcurrentHashMap就經過Segment引入了這個分段加鎖概念,但Java8因爲上述困難更改了機制,引入了紅黑樹結構,去掉了Segment。數據結構

JDK1.8的改進後,ConcurrentHashMap的寫性能有10%左右的下降,但讀性能有了很大提高。主要是將過於集中的hash節點的效率從O(N)提升到了O(LOGN)。多線程

ConcurrentHashMap利用了CAS進行實現,從而以樂觀鎖的方式實現了線程安全的HashMap,concurrentHashMap的源碼很複雜,一些方法的實現思路以下:併發

Java8的ConcurrentHashMap的數據結構實現思路大概爲,對於Hash表中每個節點,其數據結構能夠爲單節點,鏈表數組或紅黑樹,隨着節點中元素增長而改變。(改變方法見treeifyBin)。性能

put()方法

  • hash數組是否爲空,爲空則先調用initTable()方法進行初始化
  • 若是hash數組已經初始化了,則根據hash值找到對應的數組下標,若是對應節點爲空,經過cas方式直接插入
  • 若是數組已經擴容,則進行數據遷移
  • 若是數組該位置已經有值了,則須要對該節點加鎖並進行數據插入操做,僅對一個節點加鎖,其鎖粒度實際上比Java7中Segment實現更小。此時若是該節點是鏈表結構,則遍歷鏈表,插入數據;若是若是該節點是紅黑樹結構,則調用紅黑樹的插值方法插入新值
  • 針對鏈表結構,若是插入新元素後,hash數組長度超過閾值,則須要調用treeifyBin()方法進行擴容或者是將鏈表轉換爲紅黑樹

initTable()方法

  • 當table不存在,開始自旋。
  • 利用CAS操做將sizeCtl屬性設置爲-1,表示本線程正對數組初始化,阻止其餘線程的初始化。
  • 進行常規的初始化操做,擴容閾值爲數組容量的75%。
  • 將sizeCtl設置成擴容閾值,結束初始化。

treeifyBin()方法

該方法用於對數組鏈表擴容,或將鏈表結構轉化爲紅黑樹,一個節點的元素個數大於鏈表閾值(默認8)時,若是數組鏈表長度小於紅黑樹閾值(默認64),則對數組鏈表擴容,不然將該節點轉換爲紅黑樹。優化

transfer(),helpTransfer(),tryPresize()方法

這些方法負責hash表擴容,因爲要經過CAS實現線程安全,代碼十分複雜。大概思路爲,原數組長度爲n,則產生n個遷移任務,讓每個線程負責一個小任務,以後監測是否有其餘沒作完的任務,幫助遷移。ui

get()方法

get方法不涉及CAS操做,實現較爲簡單,計算hash值,找到對應節點進行判斷:

  • 該位置爲null返回null。
  • 該位置節點爲所求值,返回值。
  • 該位置節點hash值小於0,說明在擴容,或者爲紅黑樹,使用find方法。
  • 以上都不知足,該位置爲鏈表,遍歷搜索。

性能

目前多線程環境下ConcurrentMap的性能有很高的優越性,一般狀況下,若是你的Map處於多讀少寫的場景,優先考慮ConcurrentMap,但在多寫少讀的情境中,因爲資源競爭激烈,CAS自旋可能致使ConcurrentMap性能不如synchronizedMap。

參考文獻

Collections.synchronizedMap()、ConcurrentHashMap、Hashtable之間的區別
Java8 ConcurrentHashMap詳解
淺談Java8中的ConcurrentHashMap
SynchronizedMap

相關文章
相關標籤/搜索