<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          那些年你啃過的ConcurrentHashMap

          共 4021字,需瀏覽 9分鐘

           ·

          2022-07-31 14:59

          前言

          我是fancy,一個年紀(jì)輕輕bug量就累計(jì)到3200個的程序員,同事們都夸我一個人養(yǎng)活了整個測試組。

          最近迷上了并發(fā)編程。并發(fā)這玩意怎么說呢,就是你平時工作用不到,一用就用在面試上。這不,又卷起了并發(fā)容器。

          那說起并發(fā)容器,你一定也知道那幾個,CopyOnWriteArrayList、并發(fā)隊(duì)列BlockingQueue,等等。但是作為面試的典中典,聊到并發(fā)容器就無法繞開ConcurrentHashMap。

          由于篇幅原因,這篇文章不會具體解釋那些較為基礎(chǔ)的問題,比如為什么散列表數(shù)組的長度一定要是2的n次方等。將更多圍繞并發(fā)個話題。如有需要,之后會另外講解。

          所以本文我們就來深入聊聊這個大廠面試青睞的對象,八股文里的蘭博基尼:ConcurrentHashMap。


          以下的技術(shù)點(diǎn)都基于JDK1.8~

          基礎(chǔ)回顧

          我們都知道,從JDK1.8起,ConcurrentHashMap底層的數(shù)據(jù)結(jié)構(gòu)就已經(jīng)從原來的Segment分段鎖變?yōu)榱?strong>數(shù)組 + 鏈表 + 紅黑樹的形態(tài)。

          它是一款并發(fā)容器,一款裝數(shù)據(jù)的容器在并發(fā)環(huán)境下鐵定就會有各種各樣的問題。你在單線程環(huán)境下玩單機(jī),并發(fā)環(huán)境下就會有別的線程和你搶數(shù)據(jù),搶桶位。因此編寫JUC包的大神Doug Lea也都為這些場景一一做了適配,可以說是絕對的并發(fā)安全,至少運(yùn)行了這么多年了也沒遇到什么bug。

          紅黑樹

          紅黑樹數(shù)據(jù)結(jié)構(gòu)

          JDK1.8這里的紅黑樹,準(zhǔn)確的來說是一個TreeBin代理類,它作為紅黑樹的具體實(shí)現(xiàn)起存儲作用,而TreeNode是封裝紅黑樹的數(shù)據(jù)結(jié)構(gòu),所以你可以理解TreeBin就是封裝TreeNode的一個容器。

          紅黑樹在ConcurrentHashMap里面的體現(xiàn)是一個雙向鏈表:


          紅黑樹插入數(shù)據(jù)

          在這里,紅黑樹維護(hù)一個字段dir。

          在插入數(shù)據(jù)的時候會獲取節(jié)點(diǎn)的hash值,從而與當(dāng)前節(jié)點(diǎn)p的hash值比較,若插入節(jié)點(diǎn)的hash小于當(dāng)前節(jié)點(diǎn),則dir的值為-1,否則為1:

          所以,當(dāng)dir的值為-1時,就代表插入節(jié)點(diǎn)需要插入到當(dāng)前節(jié)點(diǎn)的左子節(jié)點(diǎn)或者繼續(xù)往左子樹上查找,相反如果dir值為1則向右查找,這里的規(guī)則和二叉查找樹的規(guī)則是一樣的。

          多線程競爭下的讀寫操作

          由于讀操作本身就是天然線程安全的。所以多個線程對同一個桶位同時讀并不會有什么問題。

          但若是相互競爭的寫操作,就是通過Synchronized鎖的方式來保證某個桶位同一時刻只有一個線程能獲取到資源。

          通過源碼可以看到,put()方法的核心是putVal():

          putVal()很長,它主要是通過Synchronized去鎖住每一個節(jié)點(diǎn)保證并發(fā)的安全性。在這里最為重要的兩點(diǎn),一是判斷你put進(jìn)去的這個元素,是處于鏈表還是處于紅黑樹上;二就是判斷當(dāng)前插入的key是否與鏈表或者紅黑樹上的某個元素一致。如果當(dāng)前插入key與鏈表當(dāng)中所有元素的key都不一致時,那么當(dāng)前的插入操作就追加到鏈表的末尾。否則就替換掉key對應(yīng)的value。


          擴(kuò)容原理

          在知道擴(kuò)容原理之前,得知道什么情況會導(dǎo)致擴(kuò)容。

          因此需要知道的兩個重要字段:

          • MIN_TREEIFY_CAPACITY :數(shù)組初始長度,默認(rèn)為64
          • TREEIFY_THRESHOLD :樹化閾值,指定桶位鏈表長度達(dá)到8的話,就可能發(fā)生樹化操作

          線程往桶里面新增每一個元素,都會對鏈表的長度進(jìn)行判斷,只有元素個數(shù)大于閾值MIN_TREEIFY_CAPACITY并且鏈表長度大于8,才會調(diào)用treeifyBin()把鏈表轉(zhuǎn)化為紅黑樹,否則就會進(jìn)行擴(kuò)容操作。

          這里的擴(kuò)容,指的就是擴(kuò)大數(shù)組的桶個數(shù),從而裝下更多的元素。

          除此之外,擴(kuò)容還維護(hù)了另一重要的字段,sizeCtl:

          通過翻譯,我們可以知道這個字段有三種狀態(tài):

          • sizeCtl < 0:若為-1則起標(biāo)記作用,告知其它線程此時正在初始化;若為其它的值表示當(dāng)前table正在擴(kuò)容
          • sizeCtl = 0:表示創(chuàng)建table數(shù)組時還未進(jìn)行擴(kuò)容,沒有指定的初始容量
          • sizeCtl > 0:表示當(dāng)table初始化后下次擴(kuò)容的觸發(fā)條件

          字段的值可以轉(zhuǎn)化為32位的二進(jìn)制數(shù)值,它的高16位表示擴(kuò)容標(biāo)識戳,用來標(biāo)識擴(kuò)容的范圍,如從長度16擴(kuò)容到32;低16位表示當(dāng)前參與擴(kuò)容的線程數(shù)量。

          擴(kuò)容操作會新建一個長度更大的數(shù)組,然后將老數(shù)組上的元素全部遷移到新的數(shù)組去。

          擴(kuò)容的本質(zhì)目的是為了減少桶位鏈表的長度,提高查詢效率。因?yàn)殒湵淼牟樵儚?fù)雜度是O(n),如果鏈表過長就會影響查詢效率。

          假設(shè)桶位的長度從16擴(kuò)容到32,說明桶位變多了,那遷移到新數(shù)組后就需要有元素去到新的桶位。這就需要通過一些算法將老數(shù)組和新數(shù)組的元素位置做一個映射。因?yàn)閿U(kuò)容后元素有的需要遷移到新的位置,有的還是處于和老數(shù)組一樣的位置,只不過是換了一個數(shù)組。

          如何計(jì)算出這個元素遷移后要呆在哪個桶位呢?這里使用了一個按位與的算法。就是將這個桶位key的hash值 & (擴(kuò)容前數(shù)組長度 - 1),若生成的值等于0則不需要遷移,否則就要進(jìn)行遷移。并且維護(hù)兩個變量ln和hn代表是否需要進(jìn)行位置遷移。然后采用尾插法將元素插入。這就是LastRun機(jī)制




          注:尾插法指的就是后面插入的元素都處于前一個元素的后面


          這里簡單普通的擴(kuò)容是沒什么問題的,大多數(shù)場景都和HashMap的擴(kuò)容是一樣的。

          問題就在于當(dāng)前是處于并發(fā)環(huán)境的,而擴(kuò)容也需要時間。

          正在擴(kuò)容 && 有多個線程正在競爭

          所以,比較復(fù)雜的場景來了。若是桶位正在擴(kuò)容,且有多個線程正在競爭讀寫咋辦?厚禮謝

          沒關(guān)系,我們依然分情況來討論。

          擴(kuò)容期間的讀操作

          如果擴(kuò)容期間,有線程進(jìn)行元素的讀取,比如你去get()某個key的value,那讀不讀的到呢?

          答案是可以。但是前提是你這個節(jié)點(diǎn)已經(jīng)遷移結(jié)束,如果你是一個正在擴(kuò)容遷移的節(jié)點(diǎn),那就訪問不到。

          具體的操作,就是去調(diào)用find()。

          當(dāng)一個桶位要進(jìn)行數(shù)據(jù)遷移,就會往這個桶位上放置一個ForwardingNode節(jié)點(diǎn)。除此之外還需要去標(biāo)識這個節(jié)點(diǎn)是正在遷移還是已經(jīng)遷移結(jié)束了的;

          在這里我們統(tǒng)稱遷移前的桶位節(jié)點(diǎn)叫老節(jié)點(diǎn),遷移后的桶位節(jié)點(diǎn)叫新節(jié)點(diǎn)。當(dāng)其中某一個節(jié)點(diǎn)遷移完成后,就會在老節(jié)點(diǎn)上添加一個fwd引用,它指向新節(jié)點(diǎn)的地址。

          所以當(dāng)某個線程訪問了這個節(jié)點(diǎn),看到它上面存在fwd引用,就說明當(dāng)前table正在擴(kuò)容,那么就會根據(jù)這個引用上的newtable字段去新數(shù)組的對應(yīng)桶位上找到數(shù)據(jù)然后返回。


          擴(kuò)容期間的寫操作

          寫操作相較于讀操作會更加復(fù)雜一點(diǎn),原因就是讀操作只需要獲取對應(yīng)數(shù)據(jù)返回就行了,而寫操作還要修改數(shù)據(jù),所以當(dāng)一個寫線程來修改數(shù)據(jù)剛好碰到容器處于擴(kuò)容期間,那么它還要協(xié)助容器進(jìn)行擴(kuò)容

          具體的擴(kuò)容操作依然還要分情況,假如訪問的桶位數(shù)據(jù)還沒有被遷移走的話,那就直接競爭鎖,然后在老節(jié)點(diǎn)上進(jìn)行操作就行。

          但是假如線程修改的節(jié)點(diǎn)正好是一個fwd節(jié)點(diǎn),說明當(dāng)前節(jié)點(diǎn)正處于擴(kuò)容操作,那么為了節(jié)約線程數(shù)并且快速完成任務(wù),當(dāng)前線程就會進(jìn)行協(xié)助擴(kuò)容。如果有多個線程進(jìn)行同時寫,那么它們都會調(diào)用helpTransfer()進(jìn)行協(xié)助擴(kuò)容。

          這里協(xié)助擴(kuò)容的方式就是拿到一個擴(kuò)容標(biāo)識戳,這個標(biāo)識戳的作用就是用來標(biāo)識擴(kuò)大的容量大小。因?yàn)槊總€線程都是獨(dú)立的嘛,互不通信,但是它們要做的事情是相同的,就是將桶位擴(kuò)大相同的值,所以它們就必須拿到這個相同的標(biāo)識戳,只有標(biāo)識戳一致才會進(jìn)行擴(kuò)容。

          假設(shè)一個容器從16個桶位擴(kuò)容到32個桶位,有線程A、B兩個線程。

          若A觸發(fā)了擴(kuò)容的機(jī)制,那么線程A就會進(jìn)行擴(kuò)容,此時線程B也來進(jìn)行寫操作,發(fā)現(xiàn)正在擴(kuò)容就會進(jìn)入到協(xié)助擴(kuò)容的步驟中去。

          所以線程A和線程B共同負(fù)責(zé)桶位的擴(kuò)容。

          一個線程負(fù)責(zé)擴(kuò)容的桶位個數(shù),是根據(jù)CPU核心數(shù)來算的。最少是16個,也就是一個線程最少要負(fù)責(zé)16個元素的擴(kuò)容:

          我們在上面有提過,sizeCtl轉(zhuǎn)化為32位后,它的低16位是表示當(dāng)前參與擴(kuò)容的線程數(shù)量。所以當(dāng)A線程觸發(fā)了擴(kuò)容之后,它就會將sizeCtl低16位的最后一位值+1,表示擴(kuò)容線程多了一位,當(dāng)它退出擴(kuò)容時又會將最后一位的值-1,表示擴(kuò)容線程少了一位,就這樣各個線程共同維護(hù)這個字段。

          所以你一定會好奇了:那我要是最后一個退出擴(kuò)容的線程要怎么維護(hù)啊?是的,最后一個線程還有一些別的事情要做。當(dāng)某一個線程完成任務(wù)后去判斷sizeCtl的值得時候,發(fā)現(xiàn)它的低16位只剩下最后一位是1,再減下去就是0了,那就代表它是最后一個退出擴(kuò)容的線程。此時它還需要去檢查一遍老的table數(shù)組,判斷是否還有遺漏的slot沒有遷移。具體的操作就是去輪詢檢查是否還留有fwd節(jié)點(diǎn),如果沒有的話代表遷移完成,如果有的話還需要繼續(xù)將它遷移到新的桶位。

          由于源碼非常長,所以我們就不貼全部源碼了,通過流程圖的方式來幫助大家理解這個擴(kuò)容期間的操作:

          總結(jié)

          有的童鞋在看Juc這一塊的時候會去背誦源碼,將方法的調(diào)用鏈都講的頭頭是道,我認(rèn)為沒有必要,相反面試官可能會覺得你過于抽象,背的這么清楚。并發(fā)的核心在于如何用手段去解決可能遇到的安全問題,并且讓它更高效點(diǎn),面試的目的也是為了體現(xiàn)你思維能力。

          如果覺得本文有所收獲,可以點(diǎn)個贊支持一下呀~

          重磅:國產(chǎn)IDE發(fā)布,由阿里研發(fā),完全開源!(高性能+高定制性)


          上海一博士向女生杯中投放精液?!最新進(jìn)展


          5月底被裁,6月拿到賠償和工資,下家公司要求提供近半年銀行流水來定薪,能不能只提供錢最多的6月流水?


          — 【 THE END 】—
          公眾號[程序員黃小斜]全部博文已整理成一個目錄,請?jiān)诠娞柪锘貜?fù)「m」獲取!

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點(diǎn)“在看”,關(guān)注公眾號并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 37
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美综合久久 | AV漫画网 | 国内精品久久久久久久久久久 | 日本三级性视频 | 婬乱丰满熟妇XXXXX性91 |