HashMap和concurrentHashMap解析

HashMap1.7

HashMap是非線程安全的,不支持併發操作,其實現比較簡單。先來看下JDK1.7中HashMap的結構圖

HashMap裏面是一個數組,數組中的每個元素是一個Entry對象,每個Entry對象next屬性指向下一個Entry。構成一個單向鏈表。Entry類如下

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單向鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry中

}

如圖所示,數組table中存放的每個元素是Entry的實例,爲單向鏈表的頭結點。

put值過程

  1. 插入第一個元素的時候,初始化Entry數組。
  2. 通過hash運算獲取key的hash值,通過hash值找到元素在數組中的位置,遍歷該位置上的鏈表,如果存在重複key,直接覆蓋,put方法就結束了。
  3. 如果不存在重複key,就需要將Entry添加到鏈表中,在將Entry節點添加到鏈表之前,先判斷數組大小是否已經到達了閾值(數組容量*0.75),如果需要擴容,先進行擴容,擴容後數組大小爲原來的2倍。擴容的過程,就是用一個新的數組替換原來的數組。然後重新計算hash值,並重新定位元素在數組中的位置。
  4. 將新增的Entry節點插入到數組相應下標位置處鏈表的表頭。

get過程

  • 根據key值計算hash值,根據hash值找到數組下標。從頭開始遍歷下標位置處的Entry鏈表,直到找到相等的key。

ConcurrentHashMap1.7

首先來看下一些ConcurrentHashMap1.7的結構圖:

ConcurrentHashMap1.7中是一個Segment數組,數組中每個元素上存放的是一個Segment實例,而Segment內部是由數組+鏈表組成的。即Segment中包含一個HashEntry數組,而HashEntry又是一個鏈表結構的元素

在ConcurrentHashMap1.7中,Segment通過繼承ReentrantLock的方式來加鎖。Segment又稱之爲分段鎖。ConcurrentHashMap1.7中的是一個Segment數組,每次加鎖的時候,鎖住的是一個Segment對象。也就是說每個Segment都是線程安全的,這樣就實現了全局的線程安全性。

ConcurrentHashMap1.7中有個並行級別(concurrencyLevel)的概念,並行級別默認值爲16,代表的是ConcurrentHashMap有16個Segment,理論上講可以支持16個線程併發寫入並行級別這個值在初始化的時候可以指定值,但是一旦初始化後,就不能改變了。

ConcurrentHashMap在初始化的時候,會對並行級別進行賦值,根據這個map的容量和Segment的數量計算每個Segment可以分配到元素的大小。再對Segment[0]進行初始化。Segment數組中其它位置還是null。

Put值過程

  1. 根據key獲取hash值,找到在Segment數組中的下標位置。初始化下標位置的Segment對象,初始化Segment對象這一步可能出現併發安全問題,因爲可能會出現多個線程同時初始化Segment數組中同一位置。這裏使用CAS操作初始化Segment對象和它內部的HashEntry數組。準備將值寫入到Segment中,Segment內部是由數組+鏈表組成
  2. 在寫入Segment之前,先使用tryLock()嘗試快速獲取該Segment的獨佔鎖。如果tryLock失敗,那麼在循環中執行tryLock,直到成功獲取該Segment的獨佔鎖。
  3. 獲取到Segment獨佔鎖後根據key的hash值獲取新增元素在Segment內部的HashEntry數組中下標位置。將新值設置爲HashEntry鏈表的表頭,如果超過了閾值,需要進行擴容後,重新計算下標位置,再寫入值,最好會釋放鎖。擴容操作是對segment內部的HashEntry數組進行擴容,容量擴大爲2倍,將原數組中的鏈表拆分的新數組中,此時的操作是持有Segment獨佔鎖的,不需要考慮併發問題

get過程

  • 根據hash值找到Segment數組中的下標位置。遍歷鏈表直到找到key值相同的元素。get的過程是沒有加鎖的。

HashMap1.8

HashMap1.8和HashMap1.7最大的不同在於引入了紅黑樹,數組+鏈表+紅黑樹組成。

在HashMap1.7中,查找元素的時候,首先需要根據hash值定位到數組的下標,再從鏈表的頭節點挨個遍歷,一個一個找下去,知道找到符合條件的記錄。隨着鏈表的長度的增加,查找起來更耗費時間。爲了解決這個問題,JAVA8中引入了紅黑樹,當鏈表中的元素到達8個時,會將鏈表結構轉換爲紅黑樹,減少查找的時間。

Put值過程

  1. 插入第一個元素的時候,會執行resize(),對數組進行初始化。
  2. 根據key的hash值找到數組下標的位置,如果該位置上沒有元素,初始化node,放到這個位置上就好了。
  3. 如果下標位置上有元素,那麼判斷該位置上第一個節點是紅黑樹結構還是鏈表結構。如果是紅黑樹結構,插入到紅黑樹中。如果是鏈表結構,插入到鏈表的最後面(注意:hashMap1.7是插入到鏈表的最前面)。如果插入的node爲鏈表中的第8個,將鏈表轉換成紅黑樹結構。
  4. 如果插入新的值導致HashMap的大小超過閾值,需要進行擴容。(注意:HashMap1.8中是先插入值再擴容,HashMap1.7中是先擴容再插入值)。將數組擴容2倍再初始化新的數組將鏈表中的節點,拆成兩個鏈表,放到新數組中。

get過程

  • 根據key的hash值找到對應數組下標,判斷是紅黑樹結構還是鏈表結構,如果是紅黑樹結構,使用紅黑樹的方式查找。如果是鏈表結構,從頭節點開始遍歷鏈表,直到找到key相等的元素。

ConcurrentHashMap1.8

ConcurrentHashMap1.8結構上和HashMap1.8大致相同,因爲要保證線程安全,實現上要複雜許多。

Put值過程

  1. 根據key獲取hash值,如果數組table爲空,對數組進行初始化。找到Hash值對應的下標,找到頭節點,如果該位置上沒有元素,那麼使用CAS操作將新的值放到這個位置上作爲頭節點就好了,如果CAS失敗,說明有併發,進入下一輪循環。
  2. 如果是數組正在擴容,那麼多線程協助擴容,完成數據遷移。
  3. 如果key值對應的下標位置有元素,使用Synchronized獲取數組該位置頭節點的對象監視器,對這個頭節點進行加鎖。如果是鏈表結構,將新值插入到鏈表的最後面。如果是紅黑樹結構,調用紅黑樹插入新值的方法。如果是鏈表插入完成後,還會判斷是否需要將鏈表轉成紅黑樹,這裏跟HashMap1.8有一點點不同,HashMap中是鏈表長度到達閾值8就轉換成紅黑樹,這裏轉紅黑樹的條件鏈表長度到達閾值8且當前數組的長度大於等於64,否則進行數組擴容。這裏的轉紅黑樹也要考慮線程安全問題所以依舊是使用Synchronize對頭節點做加鎖處理。
  4. ConcurrentHashMap1.8的精髓在於它的擴容遷移操作,這裏的擴容也是2倍。但是這裏的擴容操作支持多線程執行,它將數據遷移的操作拆分成了n個小任務,充分利用併發的特性,線程來了後幫忙一起遷移數據。這裏使用CAS+Synchronized的方式來保證每個線程完成屬於自己的任務。

get過程

  • 根據hash值找到數組下標位置,如果該位置上的節點正好就是所需要的,直接返回節點的值。如果頭節點的hash值小於0說明正在擴容或者是紅黑樹結構。如果是鏈表結構從頭結點開始遍歷鏈表,找出key值相等的返回結果。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章