Hashtable、ConcurrentHashMap源碼分析

爲何把這兩個數據結構對比分析呢,相信你們都明白。首先兩者都是線程安全的,可是兩者保證線程安全的方式倒是不一樣的。廢話很少說了,從源碼的角度分析一下二者的異同,首先給出兩者的繼承關係圖。算法

 Hashtable類屬性和方法源碼分析

  咱們仍是先給出一張Hashtable類的屬性和方法圖,其中Entry<K,V>是Hashtable類的靜態內部類,該類繼承自Map.Entry<K,V>接口。以下將會詳細講解Hashtable類中屬性和方法的含義。數組

  • 屬性安全

  1. Entry<?,?>[] table :Entry類型的數組,用於存儲Hashtable中的鍵值對;數據結構

  2. int count :存儲hashtable中有多少個鍵值對多線程

  3. int threshold :當count值大於該值是,哈希表擴大容量,進行rehash()併發

  4. float loadFactor :threshold=哈希表的初始大小*loadFactor,初始容量默認爲11,loadFactor值默認爲0.75ide

  5. int modCount :實現"fail-fast"機制,在併發集合中對Hashtable進行迭代操做時,若其餘線程對Hashtable進行結構性的修改,迭代器會經過比較expectedModCount和modCount是否一致,若是不一致則拋出ConcurrentModificationException異常。以下經過一個拋出ConcurrentModificationException異常的例子說明。函數

     ConcurrentModificationException異常源碼分析

    Hashtable的remove(Object key)方法見以下方法5,每一次修改hashtable中的數據都更新modCount的值。Hashtable內部類Enumerator<T>的相關部分代碼以下:spa

     Enumerator類

  • 方法

  1. contains(Object value),該方法是判斷該hashtable中是否含有值爲value的鍵值對,執行該方法須要加鎖(synchronized)。hashtable中不容許存儲空的value,因此當查找value爲空時,直接拋出空指針異常。接下來是兩個for循環遍歷table。由如上的Entry實體類中的屬性能夠看出,next屬性是指向與該實體擁有相同hashcode的下一個實體。

  2. containsKey(Object key),該方法是判斷該hashtable中是否含有鍵爲key的鍵值對,執行該方法也須要對整張table加鎖(synchronized)。首先根據當前給出的key值計算hashcode,並有hashcode值計算該key所在table數組中的下標,依次遍歷該下標中的每個Entry對象e。因爲不一樣的hashcode映射到數組中下標的位置可能相同,所以首先判斷e的hashcode值和所查詢key的hashcode值是否相同,若是相同在判斷key是否相等。

  3. get(Object key),獲取當前鍵key所對應的value值,本方法和containsKey(Object key)方法除了返回值其它都相同,若是能找到該key對應的value,則返回value的值,若是不能則返回null。

  4.  put(K key, V value),將該鍵值對加入table中。首先插入的value不能爲空。其次若是當前插入的key值已經在table中存在,則用新的value替換掉原來的value值,並將原來的value值做爲該方法的返回值返回。若是當前插入的key不在table中,則將該鍵值對插入。

    插入的方法首先判斷當前table中的值是否大於閾值(threshold),若是大於該閾值,首先對該表擴容,再將新的鍵值對插入table[index]的鏈表的第一個Entry的位置上。

  5. remove(Object key),將鍵爲key的Entry從table表中移除。一樣該方法也須要鎖定整個table表。若是該table中存在該鍵,則返回刪除的key的value值,若是當前table中不存在該key,則該方法的返回值爲null。

  6. replace(K key, V value),將鍵爲key的Entry對象值更新爲value,並將原來的value最爲該方法的返回值。

ConcurrentHashMap類屬性和方法源碼分析

  ConcurrentHashMap在JDK1.8中改動仍是挺大的。它摒棄了Segment(段鎖)的概念,在實現上採用了CAS算法。底層使用數組+鏈表+紅黑樹的方式,可是爲了作到併發,同時也增長了大量的輔助類。以下是ConcurrentHashMap的類圖。

  • 屬性

複製代碼

//ConcurrentHashMap最大容量private static final int MAXIMUM_CAPACITY = 1 << 30;//ConcurrentHashMap初始默認容量private static final int DEFAULT_CAPACITY = 16;//最大table數組的大小static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//默認並行級別,主體代碼並未使用private static final int DEFAULT_CONCURRENCY_LEVEL = 16;//加載因子,默認爲0.75private static final float LOAD_FACTOR = 0.75f;//當hash桶中hash衝突的數目大於此值時,將鏈表轉化爲紅黑樹,加快hash的查找速度static final int TREEIFY_THRESHOLD = 8;//當hash桶中hash衝突小於等於此值時,會把紅黑樹轉化爲鏈表static final int UNTREEIFY_THRESHOLD = 6;//當table數組的長度大於該值時,同時知足hash桶中hash衝突大於TREEIFY_THRESHOLD時,纔會把鏈表轉化爲紅黑樹static final int MIN_TREEIFY_CAPACITY = 64;//擴容操做中,transfer()方法容許多線程,該值表示一個線程執行transfer時,至少對連續的多少個hash桶進行transferprivate static final int MIN_TRANSFER_STRIDE = 16;//ForwardingNode的hash值,ForwardingNode是一種臨時節點,在擴容中才會出現,不存儲實際的數據static final int MOVED     = -1;//TreeBin的hash值,TreeBin是用於代理TreeNode的特殊節點,存儲紅黑樹的根節點static final int TREEBIN   = -2;//用於和負數hash進行&運算,將其轉化爲正數static final int HASH_BITS = 0x7fffffff;

複製代碼

  •  基本類

  1. Node<K,V>:基本結點/普通節點。當table中的Entry以鏈表形式存儲時才使用,存儲實際數據。此類不會在ConcurrentHashMap之外被修改,並且該類的key和value永遠不爲null(其子類可爲null,隨後會介紹)。

     Node<K,V>

  2. TreeNode:紅黑樹結點。當table中的Entry以紅黑樹的形式存儲時纔會使用,存儲實際數據。ConcurrentHashMap中對TreeNode結點的操做都會由TreeBin代理執行。當知足條件時hash會由鏈表變爲紅黑樹,可是TreeNode中經過屬性prev依然保留鏈表的指針。

     TreeNode<K,V>

  3. ForwardingNode:轉發結點。該節點是一種臨時結點,只有在擴容進行中才會出現,其爲Node的子類,該節點的hash值固定爲-1,而且他不存儲實際數據。若是舊table的一個hash桶中所有結點都遷移到新的數組中,舊table就在桶中放置一個ForwardingNode。當讀操做或者迭代操做遇到ForwardingNode時,將操做轉發到擴容後新的table數組中去執行,當寫操做碰見ForwardingNode時,則嘗試幫助擴容。

     ForwardingNode<K,V>

    補充圖一張說明擴容下是如何遍歷結點的。

  4. TreeBin:代理操做TreeNode結點。該節點的hash值固定爲-2,存儲實際數據的紅黑樹的根節點。由於紅黑樹進行寫入操做整個樹的結構可能發生很大變化,會影響到讀線程。所以TreeBin須要維護一個簡單的讀寫鎖,不用考慮寫-寫競爭的狀況。固然並非所有的寫操做都須要加寫鎖,只有部分put/remove須要加寫鎖。

     TreeBin<K,V>

  5. ReservationNode:保留結點,也被稱爲空節點。該節點的hash值固定爲-3,不保存實際數據。正常的寫操做都須要對hash桶的第一個節點進行加鎖,若是hash桶的第一個節點爲null時是沒法加鎖的,所以須要new一個ReservationNode節點,做爲hash桶的第一個節點,對該節點進行加鎖。

     ReservationNode<K,V>

  • ConcurrentHashMap方法

  首先介紹一些基本的方法,這些方法不會直接用到,但倒是理解ConcurrentHashMap常見方法前提,由於這些方法被ConcurrentHashMap常見的方法調用。而後在介紹完這些基本方法的基礎上,再分析常見的containsValue、put、remove等常見方法。

  1. Node<K,V>[] initTable():初始化table的方法。初始化這個工做不是在構造函數中執行的,而是在put方法中執行,put方法中發現table爲null時,調用該方法。

     initTable方法

  2. 以下幾個方法是用於讀取table數組,使用Unsafe提供更強的功能代替普通的讀寫。

     View Code

  3. 擴容方法:擴容分爲兩個步驟:第一步新建一個2倍大小的數組(單線程完成),第二步是rehash,把舊數組中的數據從新計算hash值放入新數組中。ConcurrentHashMap在第二步中處理舊table[index]中的節點時,這些節點要麼在新table[index]處,要麼在新table[index]和table[index+n]處,所以舊table各hash桶中的節點遷移不相互影響。ConcurrentHashMap擴容能夠在多線程下完成,所以就須要計算每一個線程須要負責處理多少個hash桶。

     計算每一個transfer處理桶的個數

    計算完成以後每一個transfer按照計算的值處理相應下標位置的桶,擴容操做從舊數組的末尾向前一次對hash桶進行處理。從末尾向前處理主要是減小和遍歷數據時的鎖衝突。從舊數組的末尾向前代碼以下:

     計算每一個transfer處理hash桶的區域

    擴容部分的完整代碼以下:

     擴容代碼

    以下是一個鏈表擴容的示意圖,第一張是一個hash桶中的一條鏈表,其中藍色節點表示第X位爲0,而紅色表示第X位爲1,擴容後舊table[i]的桶中爲一個ForwardingNode節點,而新nextTab[i]和nextTable[i+n]的桶中分別爲第二張和第三張圖。

  4. Traverser只讀遍歷器:確切的說它不是方法,而是一個內部類。ConcurrentHashMap的多線程擴容增長了對ConcurrentHashMap遍歷的困難。當遍歷舊table時,若是遇到某個hash桶中爲ForwardingNode節點,則遍歷順序參考基本類中ForwardingNode中的介紹。

     Traverser

  5. containsValue(Object value):遍歷ConcurrentHashMap看是否存在值爲value的Node。

     containsValue(Object value)

  6. containsKey(Object key):遍歷ConcurrentHashMap看是否存在鍵爲key的Node。

     containsKey(Object key)

  7. put(K key, V value):將該鍵值對插入ConcurrentHashMap中。

     put(K key, V value)

  8. remove(Object key):刪除鍵爲key的Node。一樣其中也包含了對replace(Object key, V value, Object cv)的介紹。

     remove(Object key)

  至此ConcurrentHashMap的主要方法也就介紹完了,綜合比較Hashtable和ConcurrentHashMap,二者都是線程安全的,可是Hashtable是表級鎖,而ConcurrentHashMap是段級鎖,鎖住的單個Node,並且ConcurrentHashMap能夠併發讀取。對整張表進行迭代時,ConcurrentHashMap使用了不一樣於Hashtable的迭代方式,而是一種弱一致性的迭代器。

相關文章
相關標籤/搜索