高級併發編程系列十六(一文搞懂ConcurrentHashMap)

1.考考你

早上好!今天我要跟你分享的是ConcurrentHashMapjava

儘管你說大家的項目業務複雜度不高,沒有多少用戶量,不須要考慮併發狀況,你歷來都只用到了HashMap,不關心ConcurrentHashMap。那也沒有關係,ConcurrentHashMap與HashMap師出同門,有着千絲萬縷的關係,它們兩者的武功路數是同樣的,只不過ConcurrentHashMap的修爲要更高(它是併發安全的HashMap)。編程

所以藉助上一篇咱們剛分享完HashMap,趁熱打鐵一起把ConcurrentHashMap一塊兒收拾了。那麼這一篇咱們就接着上一篇,主要搞清楚這麼幾個問題:數組

  • 上一篇咱們分析了HashMap的底層實現原理,好比說底層數據結構是數組,經過拉鍊法解決hash衝突等。那你能告訴我,在實際項目中該如何更好的使用HashMap嗎?緩存

  • 上一篇咱們分析了HashMap在解決hash衝突的時候,有兩種方案:開放尋址法、拉鍊法。那你能告訴我,它們之間有什麼區別嗎?安全

  • 你說ConcurrentHashMap是線程安全版本的HashMap。那麼你能告訴我,它是如何實現線程安全的?在jdk8與jdk8之前版本中,有什麼差別嗎?以及jdk8中改變線程安全實現方式的背後邏輯嗎?(靈魂三拷問......)數據結構

好了,帶上以上幾個問題,讓咱們開始吧併發

2.案例

2.1.HashMap最佳實踐

如今咱們知道了,在實際項目中,咱們是把HashMap做爲容器來使用的。既然是容器,那就須要考慮這麼幾個問題:分佈式

  • 容器的容量大小,可以支持存放多少個元素,一開始給多少合適呢?(初始容量問題)函數

  • 指定了容器初始容量大小後,萬一元素太多,容器放不下了如何處理呢?(容器擴容、裝載因子問題)微服務

針對上面的問題,咱們來分析一下:

  • 在HashMap中,默認的初始容量大小是16, 在實際項目中,咱們能夠考慮預估要存入的元素個數,根據元素個數設置合適的初始容量。減小HashMap動態擴容,減小重建哈希表,從而提高性能

/**
* The default initial capacity - MUST be a power of two.
* 默認初始容量,HashMap的容量最好是保持 2的n次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • HashMap裝載因子,默認是0.75。表示在HashMap中,當元素的個數超過:capacity * 0.75的時候,就會啓動動態擴容,每次擴容後容量大小都是以前的兩倍

  • 裝載因子越大,表示空閒空間越小,對應的HashMap衝突的機率就會越大。在實際項目中,咱們能夠設置合適的裝載因子,提高HashMap性能

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

2.2.hash衝突詳解

如今你已經知道了在項目中用好HashMap,須要考慮的一些問題:初始容量、裝載因子等。

接下來咱們一塊兒來看另一個問題:如何解決hash衝突。關於hash衝突,單從應用HashMap來講,咱們並不須要關心,畢竟大多數時候,咱們都僅僅是使用HashMap,並不會考慮從0到1寫一個HashMap。可是我仍是想建議你瞭解一下,關於整個世界的認知,咱們都應該知其然,且知其因此然

上一篇咱們提到關於hash衝突,主要有兩種解決方案:開放尋址法,拉連法。可是當時我並無詳細說明,咱們跳過了,如今咱們一塊兒來看一下,什麼是開放尋址法?什麼是拉鍊法?

咱們知道HashMap的底層數據結構是數組,數組的容量是有限的(咱們暫時不考慮擴容,由於擴容後容量也仍是有限的,只是比起擴容前大一倍)。

咱們也知道HashMap的存儲是key/value鍵值對,須要將任意類型的key,經過散列函數hash(key),轉換成數組下標,與數組聯繫起來,實如今O(1)時間複雜度下,查找目標元素。咱們直觀的看一個圖:

 

另外你還記得咱們上一篇舉的示例嗎?hash(0+5)=5,hash(1+4)=5,hash(2+3)=5。假設當前目標數組下標是:5,那你也看到了,左右key:0+5,1+4,2+3並不相同,可是經過hash函數後,卻都指向了數組下標:5的位置。這就是hash衝突的由來。

好了,我又帶着你回顧了一遍hash衝突,如今咱們從新回到解決hash衝突:開放尋址法、拉鍊法

2.2.1.關於開放尋址法

開放尋址法,是指當發生hash衝突後,好比說某個key,經過哈希函數hash(key),指向了數組下標5的位置。此時不巧下標5的位置已經存放了元素,即發生了hash衝突。

那麼開放尋址法的作法,是從數組下標5的位置開始向後搜索,尋找到第一個空的,還未存聽任何元素的下標位置,好比:8,做爲當前key元素存放的位置

咱們來直觀的看一個圖:

前一個元素hash(1+4)=5,佔用了數組下標5的位置;

後一個元素hash(2+3)=5,雖然指向了數組下標5位置,可是此時下標5的位置已經被hash(1+4)元素佔用,因此hash(2+3)元素只能繼續向後搜索,直到搜索到下標8的位置,發現下標8位置未使用,即做爲元素hash(2+3)的位置。

你看,這就是開放尋址法

2.2.2.關於拉鍊法

拉鍊法,是指當發生hash衝突後,好比說某個key,經過哈希函數hash(key),指向了數組下標5的位置。此時不巧下標5的位置已經存放了元素,即發生了hash衝突。

那麼拉鍊法的作法,不一樣於開放尋址法。它不須要從下標5的位置向後搜索,它是直接定位到下標5的位置,在此處經過鏈表,將發生hash衝突的多個元素鏈接起來,造成一個鏈表

咱們直觀的看一個圖:

你看,這就是拉鍊法。

2.2.3.關於兩者適用場景

如今你已經知道了什麼是hash衝突,以及hash衝突的兩種主要解決方案:開放尋址法、拉鍊法

咱們再來探討一個問題,什麼場景下適合用開放尋址法?什麼場景下適合用拉鍊法呢?

咱們知道開放尋址法,最大的特色就是當發生hash衝突的時候,有向後搜索的操做。那麼假設在存放大量目標元素對象的場景下,發生衝突的機率會很是大,每次發生衝突,都要向後搜索操做,會比較影響性能。

所以,開放尋址法適合在容器容量需求不大(即目標元素很少),hash衝突發生機率小的場景下,我建議你能夠看一下ThreadLocalMap源碼,ThreadLocalMap即便用了開放尋址法解決hash衝突。

知道了開放尋址法的適用場景後,咱們經過反向思考,即不難理解拉鍊法的使用場景了。拉鍊法適合在目標元素多,容器容量需求大、hash衝突發生機率大的業務場景。不用說,你已經知道了,咱們一直的主角HashMap,ConcurrentHashMap都使用了拉鍊法解決hash衝突。

2.3.ConcurrentHashMap詳解

爲了方便你理解ConcurrentHashMap,咱們前面作了很是長的鋪墊,上一篇文章以及這篇文章的上半部分。

如今相信你已經理解了HashMap,那咱們就開始進入ConcurrentHashMap的內容了。關於ConcurrentHashMap,大方向上你先有一個印象:ConcurrentHashMap它是HashMap的線程安全版本,它與HashMap一脈相傳,是師兄弟關係,只不過它是關門弟子,得了師傅的真傳,能力要更增強大一些

上面這段話的意思,大體是想要告訴你,ConcurrentHashMap的底層實現原理,用了什麼數據結構,如何解決hash衝突等都與HashMap同樣。咱們只須要關心它是如何實現線程安全的就能夠了。

那就讓咱們開始吧,你須要注意一下,ConcurrentHashMap線程安全的實現,在jdk8版本,與jdk8之前的版本區別比較大,咱們分開來講

2.3.1.jdk7版本的ConcurrentHashMap

咱們先來看ConcurrentHashMap在jdk8之前版本的實現,如下個人分析,和涉及到的源碼都是參考jdk7,你先留意一下。

談到線程安全,你確定想到了,除了加鎖沒有別的手段,而且你還進一步想到了咱們在鎖小節分享的:synchronized、或者Lock對象

這裏咱們初步的想法是沒有任何問題的,想要實現線程安全:加鎖。可是咱們還須要稍微往前思考一個問題:若是隻是簡單的加鎖,那不就是Hashtable了嗎?java設計者的大神們,大家是否是閒着沒事幹,順便多寫了一個ConcurrentHashMap呢?

答案確定不是的,大神之因此稱之爲大神,其中有一個區別於常人的特質,就是歷來不作無用功!

那要這麼說,咱們就須要搞清楚有了Hashtable,爲何還須要一個ConcurrentHashMap?

咱們先回顧一下,Hashtable是如何實現線程安全的,以及它存在什麼問題?你還記得嗎,前面咱們在高級併發編程系列十四(併發集合基礎)一篇,分享了Hashtable實現線程安全的方式,它是直接在每一個操做方法上加了synchronized關鍵字。好比下圖,是咱們熟悉的get方法:

咱們說直接在方法上加synchronized關鍵字,實現線程安全有什麼問題呢?最大的問題就是鎖粒度太大,致使併發性能低,不足以應用在高併發業務場景。這也是爲何Hashtable出身以來,從未受寵的緣由,你也不喜歡它對吧!千萬別說喜歡,非要喜歡的話怎麼不見在你的項目中使用Hashtable呢?

說了這麼多別人的不是,其目的都是爲了陪襯ConcurrentHashMap的主角光環。那你說說看吧,ConcurrentHashMap究竟是如何實現線程安全,又是如何支持高併發的?咱們從兩個方面來看。

既然要線程安全,那麼鎖,確定是要鎖的,基礎原理不變

另外要支持高併發業務場景,都加鎖了,還怎麼實現高併發呢?這個地方你須要特別留意了,這裏我將給你分享一個解決大、且複雜問題的通用思想,咱們說:面對大的,複雜的業務問題,要想實現化繁爲簡,惟一的手段便是拆分。今天咱們說分佈式,微服務化其核心都是拆字決!

那具體到ConcurrentHashMap中,它究竟是如何拆的呢?它是這麼拆的:經過分段鎖,即保障了線程安全,又提高了併發能力

關於分段鎖,你能夠這麼去理解:原來是一個大鎖,限制了併發能力,由於只有一把鎖;如今咱們把大鎖分紅多把小鎖(ConcurrentHashMap默認是16個分段鎖),能夠同時支持16個併發

好了,文字分析咱們差很少講明白了,接下來我經過源碼,以及畫一個圖,讓你更好的理解ConcurrentHashMap(你須要注意,我當前的jdk版本是7)。

ConcurrentHashMap圖示:

ConcurrentHashMap源碼錶明:

經過上圖咱們直觀看到在jdk7中,ConcurrentHashMap它是經過分段鎖實現支持高併發,默認狀況下,有16個分段鎖,其中每個分段鎖中,便是一個HashMap。

接下來咱們一塊兒經過源碼,輔助理解上圖。

  • 底層數據結構,數組

/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments;
  • 分段鎖Segment定義
/**
* Segments are specialized versions of hash tables.  This
* subclasses from ReentrantLock opportunistically, just to
* simplify some locking and avoid separate construction.
* 每一個Segment,原來就是一個ReentrantLock,好熟悉有沒有
*/
static final class Segment<K,V> extends ReentrantLock implements Serializable {
     ......   
}
  • 分段鎖內部定義
/*
*每一個Segment,都是一個HashMap
*/
transient volatile HashEntry<K,V>[] table;

2.3.2.jdk8版本的ConcurrentHashMap

如今你已經知道了jdk7中的ConcurrentHashMap,咱們說在jdk8中,它再也不是分段鎖的設計思想了,它變了!

變成什麼了呢?變成了cas + synchronized組合來保障線程安全,同時實現支持高併發。這裏你還記得什麼是cas嗎,若是不記得了,我推薦你看我這個系列的另一篇文章:高級併發編程系列十二(一文搞懂cas)

這裏限於篇幅和側重關注點,我就再也不詳細跟你說cas了,我只簡單帶你回顧一下cas的核心原理: cas本質上是不到黃河心不死,即不釋放cpu,循環操做,直到操做成功爲止

它的操做原理是三個值:內存值A、指望值B、更新值C。每次操做都會比較內存值A,是否等於指望值B、若是等於則將內存值更新成值C,操做成功;若是內存值A,不等於指望值B,則操做失敗,進行下一次循環操做

給你回顧完cas,咱們主要再來關注爲何在jdk8中,ConcurrentHashMap會經過cas +synchronized組合,來替換jdk7中的分段鎖Segment呢?難道分段鎖它不香嗎?

我帶着你一塊兒分享一下個人我的理解:

  • 咱們知道cas是一種無鎖化機制,你們均可以並行來搶佔cpu(由於不加鎖嘛),天然是你能夠搶,我也能夠搶

  • 那要這麼說,cas就很是適合併發衝突小,加鎖臨界點(範圍)小的應用場景。

  • 請說人話:什麼是併發衝突小?簡單說就是讀多寫少的業務場景,即讀不須要加鎖,寫才須要加鎖

  • 嗯,你這麼說我就明白了,咱們在項目中使用HashMap,正好都是讀多寫少,一次寫入,屢次讀取的業務場景。好比本地緩存實現方案

  • 所以cas+synchronized組合實現ConcurrentHashMap的方案,在實際應用中,會比分段鎖的實現方案,帶來更高的併發支持,性能會更好!

你看,這麼說,你是否是也能理解jdk8中的ConcurrentHashMap了。最後咱們仍是看一個圖吧。

這個圖你見過了,就是咱們上一篇中HashMap的圖。在jdk8中ConcurrentHashMap的底層數據結構上,與HashMap徹底同樣,它只是增長了cas+synchronized操做。話很少說,咱們看圖:

相關文章
相關標籤/搜索