java并發(fā)編程之ConcurrentHashMap

引言

ConcurrentHashMap是線程安全并且高效的HashMap诗力,在并發(fā)編程中經(jīng)尘悴。可見它的使用粱甫,在開始分析它的高并發(fā)實現(xiàn)機制前胜宇,先講講廢話耀怜,看看它是如何被引入jdk的。

為什么引入ConcurrentHashMap桐愉?

  1. HashMap線程不安全财破,它的線程不安全主要發(fā)生在put等對HashEntry有直接寫操作的地方:


    HashMap線程不安全操作源碼示例

    從put操作的源碼不難看出,線程不安全主要可能發(fā)生在這兩個地方:

  • key已經(jīng)存在从诲,需要修改HashEntry對應(yīng)的value左痢;

  • key不存在,在HashEntry中做插入系洛。

  1. Hashtable線程安全俊性,但是效率低下:


    Hashtable源碼示例.png

    從Hashtable示例的源碼可以看出,Hashtable是用synchronized關(guān)鍵字來保證線程安全的描扯,由于synchronized的機制是在同一時刻只能有一個線程操作磅废,其他的線程阻塞或者輪詢等待,在線程競爭激烈的情況下荆烈,這種方式的效率會非常的低下拯勉。

注:小小的多嘴一句竟趾,Hashtable擴容的時候newSize = 2 * oldSize + 1,這個是常識性的點宫峦,但是由于整個jdk源碼封裝比較好岔帽,而且Hashtable效率低下,使用較少导绷,貌似好多程序員都不太知道這一點犀勒。

ConcurrentHashMap的為什么高效?
Hashtable低效主要是因為所有訪問Hashtable的線程都爭奪一把鎖妥曲。如果容器有很多把鎖贾费,每一把鎖控制容器中的一部分?jǐn)?shù)據(jù),那么當(dāng)多個線程訪問容器里的不同部分的數(shù)據(jù)時檐盟,線程之前就不會存在鎖的競爭褂萧,這樣就可以有效的提高并發(fā)的訪問效率。這也正是ConcurrentHashMap使用的分段鎖技術(shù)葵萎。將ConcurrentHashMap容器的數(shù)據(jù)分段存儲导犹,每一段數(shù)據(jù)分配一個Segment(鎖),當(dāng)線程占用其中一個Segment時羡忘,其他線程可正常訪問其他段數(shù)據(jù)谎痢。

ConcurrentHashMap實現(xiàn)分析

在分析ConcurrentHashMap的源碼之前先來看看它的結(jié)構(gòu):

ConcurrentHashMap類圖

從類圖可以看出:ConcurrentHashMap由Segment和HashEntry組成。

  • Segment是可重入鎖卷雕,它在ConcurrentHashMap中扮演分離鎖的角色节猿;

  • HashEntry主要存儲鍵值對;

  • CurrentHashMap包含一個Segment數(shù)組漫雕,每個Segment包含一個HashEntry數(shù)組并且守護它沐批,當(dāng)修改HashEntry數(shù)組數(shù)據(jù)時,需要先獲取它對應(yīng)的Segment鎖蝎亚;而HashEntry數(shù)組采用開鏈法處理沖突,所以它的每個HashEntry元素又是鏈表結(jié)構(gòu)的元素先馆。

由此可以得出ConcurrentHashMap的結(jié)構(gòu)圖如下:

ConcurrentHashMap結(jié)構(gòu)圖
初始化ConcurrentHashMap
ConcurrentHashMap構(gòu)造方法

可以看出发框,ConcurrentHashMap的構(gòu)造方法都調(diào)用了public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel),初始化部分都由它來完成煤墙,我們來看一看它是怎么來初始化ConcurrentHashMap的梅惯。

ConcurrentHashMap初始化具體實現(xiàn)
整個初始化是通過參數(shù)initialCapacity,loadFactor和concurrencyLevel來初始化segmentShift(段偏移量)仿野、segmentMask(段掩碼)和segment數(shù)組铣减。

ConcurrentHashMap初始化具體實現(xiàn)
  • 計算segment數(shù)組長度
    segment數(shù)組長度ssize是由concurrencyLevel計算得出,當(dāng)ssize < concurrencyLevel時脚作,ssize *= 2葫哗,至于為什么一定要保證ssize是2的N次方是為了可以通過按位與來定位segment缔刹;

注:concurrencyLevel的最大值是65535,那么劣针,ssize的最大值就為65536校镐,對應(yīng)到二進制就是16位。

  • 初始化segmentShift捺典、segmentMask
    segmentShift和segmentMask在定位segment使用鸟廓,segmentShift = 32 - ssize向左移位的次數(shù),segmentMask = ssize - 1襟己。ssize的最大長度是65536引谜,對應(yīng)的 segmentShift最大值為16,segmentMask最大值是65535擎浴,對應(yīng)的二進制16位全1员咽;

  • 初始化segment

    1. 初始化每個segment的HashEntry長度;
    2. 創(chuàng)建segment數(shù)組和segment[0]退客。

    注:HashEntry長度cap同樣也是2的N次方骏融,默認(rèn)情況,ssize = 16萌狂,initialCapacity = 16档玻,loadFactor = 0.75f,那么cap = 1茫藏,threshold = (int) cap * loadFactor = 0误趴。

Segment定位

  • Hash算法
    ConcurrentHashMap使用分段鎖segment來保護數(shù)據(jù),也就是說务傲,在插入和讀取元素凉当,需要先通過hash算法定位segment。ConcurrentHashMap使用了變種hash算法對元素的hashCode再散列售葡。


    Hash算法

注:為什么需要再散列看杭?
再散列的目的是為了減少沖突,讓元素可以近似均勻的分布在不同的Segment上挟伙,從而提升存儲效率楼雹。如果hash算法不好,最差的情況是所有的元素都在一個Segment中尖阔,這時候hash表將退化成鏈表贮缅,查詢插入的時間復(fù)雜度都會從理想的o(1)退化成o(n^2),同時介却,分段鎖也會失去存在的意義谴供。

  • Segment定位
    ConcurrentHashMap將hashCode進行位運算來定位具體的segment:


    Segment定位

    默認(rèn)情況下,segmentShift = 28齿坷, segmentMask = 15桂肌,hashCode最大是32位的二進制數(shù)数焊,向右無符號移動28位,讓高4位參與位運算(& segmentMask)轴或。

ConcurrentHashMap相關(guān)操作實現(xiàn)分析

主要分析ConcurrentHashMap常用的三個操作:get/put/size的具體實現(xiàn)昌跌。

  • get操作
    get實現(xiàn)
    1. 根據(jù)key,計算出hashCode照雁;
    2. 根據(jù)步驟1計算出的hashCode定位segment蚕愤,如果segment不為null && segment.table也不為null,跳轉(zhuǎn)到步驟3饺蚊,否則萍诱,返回null,該key所對應(yīng)的value不存在污呼;
    3. 根據(jù)hashCode定位table中對應(yīng)的hashEntry裕坊,遍歷hashEntry,如果key存在燕酷,返回key對應(yīng)的value籍凝;
    4. 步驟3結(jié)束仍未找到key所對應(yīng)的value,返回null苗缩,該key鎖對應(yīng)的value不存在饵蒂。

比起Hashtable,ConcurrentHashMap的get操作高效之處在于整個get操作不需要加鎖酱讶。如果不加鎖退盯,ConcurrentHashMap的get操作是如何做到線程安全的呢?原因是volatile泻肯,所有的value都定義成了volatile類型渊迁,volatile可以保證線程之間的可見性,這也是用volatile替換鎖的經(jīng)典應(yīng)用場景灶挟。


HashEntry value定義
  • put操作
    ConcurrentHashMap提供兩個方法put和putIfAbsent來完成put操作琉朽,它們之間的區(qū)別在于put方法做插入時key存在會更新key所對應(yīng)的value,而putIfAbsent不會更新稚铣。

    • put實現(xiàn)


      put實現(xiàn)
      1. 參數(shù)校驗箱叁,value不能為null,為null時拋出NPE榛泛;
      2. 計算key的hashCode;
      3. 定位segment噩斟,如果segment不存在曹锨,創(chuàng)建新的segment;
      4. 調(diào)用segment的put方法在對應(yīng)的segment做插入操作剃允。
    • putIfAbsent實現(xiàn)


      putIfAbsent實現(xiàn)

      putIfAbsent的執(zhí)行過程與put方法是一致的沛简,除了最后調(diào)用的segment的put方法參數(shù)onlyIfAbsent傳參不一樣齐鲤。

    segment的put方法實現(xiàn)
    segment的put方法是整個put操作的核心,它實現(xiàn)了在segment的HashEntry數(shù)組中做插入(segment的HashEntry數(shù)組采用開鏈法來處理沖突)椒楣。

    segment put實現(xiàn)

    具體的執(zhí)行流程如下:

    1. 獲取鎖给郊,保證put操作的線程安全;
    2. 定位到HashEntry數(shù)組中具體的HashEntry捧灰;
    3. 遍歷HashEntry鏈表淆九,假若待插入key已存在:
    • 需要更新key所對應(yīng)value(!onlyIfAbsent),更新oldValue -> newValue毛俏,跳轉(zhuǎn)到步驟5炭庙;
    • 否則,直接跳轉(zhuǎn)到步驟5煌寇;
    1. 遍歷完HashEntry鏈表焕蹄,key不存在,插入HashEntry節(jié)點阀溶,oldValue = null腻脏,跳轉(zhuǎn)到步驟5;
    2. 釋放鎖银锻,返回oldValue永品。

    步驟4在做插入的時候?qū)嶋H上經(jīng)歷了兩個步驟:

    • 第一:HashEntry數(shù)組擴容;
      • 是否需要擴容
        在插入元素前會先判斷Segment的HashEntry數(shù)組是否超過threshold徒仓,如果超過閥值腐碱,則需要對HashEntry數(shù)組擴容;
      • 如何擴容
        在擴容的時候掉弛,首先創(chuàng)建一個容量是原來容量兩倍的數(shù)組症见,將原數(shù)組的元素再散列后插入到新的數(shù)組里。為了高效殃饿,ConcurrentHashMap只對某個Segment進行擴容谋作,不會對整個容器擴容。
    • 第二:定位添加元素對應(yīng)的位置乎芳,然后將其放到HashEntry數(shù)組中遵蚜。
  • size實現(xiàn)
    如果需要統(tǒng)計整個ConcurrentHashMap的容量,需要統(tǒng)計所有Segment容量然后求和奈惑,Segment提供變量count用于存儲當(dāng)前Segment的容量吭净。但是ConcurrentHashMap為了保證線程安全,并不是直接把所有的Segment的count相加來得到整個容器的大小肴甸,我們來看看ConcurrentHashMap是怎么來統(tǒng)計容量的寂殉。


    size實現(xiàn)

    由于在累加count的操作的過程中之前累加過的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap先嘗試2次不鎖住Segment的方式來統(tǒng)計每個Segment的大小原在,如果在統(tǒng)計的過程中Segment的count發(fā)生了變化友扰,這時候再加鎖統(tǒng)計Segment的count彤叉。

    ConcurrentHashMap如何判斷統(tǒng)計過程中Segment的cout發(fā)生了變化?
    Segment使用變量modCount來表示Segment大小是否發(fā)生變化村怪,在put/remove/clean操作里都會將modCount加1秽浇,那么在統(tǒng)計size的前后只需要比較modCount是否發(fā)生了變化,如果發(fā)生變化甚负,Segment的大小肯定發(fā)生了變化柬焕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腊敲,隨后出現(xiàn)的幾起案子击喂,更是在濱河造成了極大的恐慌,老刑警劉巖碰辅,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懂昂,死亡現(xiàn)場離奇詭異,居然都是意外死亡没宾,警方通過查閱死者的電腦和手機凌彬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來循衰,“玉大人铲敛,你說我怎么就攤上這事』岫郏” “怎么了伐蒋?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迁酸。 經(jīng)常有香客問我先鱼,道長,這世上最難降的妖魔是什么奸鬓? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任焙畔,我火速辦了婚禮,結(jié)果婚禮上串远,老公的妹妹穿的比我還像新娘宏多。我一直安慰自己,他們只是感情好澡罚,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布伸但。 她就那樣靜靜地躺著,像睡著了一般留搔。 火紅的嫁衣襯著肌膚如雪更胖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音函喉,去河邊找鬼。 笑死荣月,一個胖子當(dāng)著我的面吹牛管呵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哺窄,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼捐下,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了萌业?” 一聲冷哼從身側(cè)響起坷襟,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎生年,沒想到半個月后婴程,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡抱婉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年档叔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒸绩。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡衙四,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出患亿,到底是詐尸還是另有隱情传蹈,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布步藕,位于F島的核電站惦界,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏漱抓。R本人自食惡果不足惜表锻,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乞娄。 院中可真熱鬧瞬逊,春花似錦、人聲如沸仪或。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽范删。三九已至蕾域,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旨巷。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工巨缘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人采呐。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓若锁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親斧吐。 傳聞我的和親對象是個殘疾皇子又固,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 鎖 鎖是用來控制多個線程訪問共享資源的方式,一般來說煤率,一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多...
    黃俊彬閱讀 1,478評論 1 15
  • Java SE 基礎(chǔ): 封裝仰冠、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體蝶糯,并盡...
    Jayden_Cao閱讀 2,103評論 0 8
  • 相關(guān)文章Java并發(fā)編程(一)線程定義洋只、狀態(tài)和屬性 Java并發(fā)編程(二)同步Java并發(fā)編程(三)volatil...
    劉望舒閱讀 2,626評論 0 22
  • 數(shù)據(jù)結(jié)構(gòu) ConcurrentHashMap 實現(xiàn)并發(fā)操作的原理 使用了鎖分段技術(shù):ConcurrentHashM...
    tomas家的小撥浪鼓閱讀 1,910評論 0 6
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,488評論 0 3