Java面試??贾瓹oncurrentHashMap多線程擴(kuò)容機(jī)制詳解
一、引言
ConcurrentHashMap
技術(shù)在互聯(lián)網(wǎng)技術(shù)使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在 ConcurrentHashMap
技術(shù)的使用和原理方面對小伙伴們進(jìn)行 360° 的刁難。
作為一個在互聯(lián)網(wǎng)公司面一次拿一次 Offer
的面霸,打敗了無數(shù)競爭對手,每次都只能看到無數(shù)落寞的身影失望的離開,略感愧疚(請允許我使用一下夸張的修辭手法)。
本篇文章大約讀取時間十分鐘,帶你徹底掌握 ConcurrentHashMap
的 魔鬼式的多線程擴(kuò)容 邏輯,目錄如下:
二、擴(kuò)容流程
我們上一篇文章講了關(guān)于 ConcurrentHashMap
的存儲流程:聊聊ConcurrentHashMap的存儲流程,熟悉了其數(shù)據(jù)是如何的放到數(shù)組、鏈表、紅黑樹中
今天這篇文章,我們重點講一下 ConcurrentHashMap
魔鬼式的多線程擴(kuò)容邏輯
系好安全帶,我們發(fā)車了!
1、treeifyBin方法觸發(fā)擴(kuò)容
// 在鏈表長度大于等于8時,嘗試將鏈表轉(zhuǎn)為紅黑樹 private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; // 數(shù)組不能為空 if (tab != null) { // 數(shù)組的長度n,是否小于64 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // 如果數(shù)組長度小于64,不能將鏈表轉(zhuǎn)為紅黑樹,先嘗試擴(kuò)容操作 // 這里的擴(kuò)容傳入的數(shù)據(jù)是當(dāng)前數(shù)據(jù)的2倍 tryPresize(n << 1); // 省略部分代碼…… } }
2、tryPreSize方法-針對putAll的初始化操作
private final void tryPresize(int size) { // 如果當(dāng)前傳過來的size大于最大值MAXIMUM_CAPACITY >>> 1,則取 MAXIMUM_CAPACITY // 反之取 tableSizeFor(size + (size >>> 1) + 1)= size的1.5倍 + 1 // tableSizeFor:這個方法我們之前也講過,找size的最近二次冪 // 這個地方的c int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); int sc; // 當(dāng)前的sizeCtl大于等于0,證明不屬于異常狀態(tài) // static final int MOVED = -1; // 代表當(dāng)前hash位置的數(shù)據(jù)正在擴(kuò)容! // static final int TREEBIN = -2; // 代表當(dāng)前hash位置下掛載的是一個紅黑樹 // static final int RESERVED = -3; // 預(yù)留當(dāng)前索引位置…… while ((sc = sizeCtl) >= 0) { Node<K,V>[] tab = table; int n; // 如果當(dāng)前的數(shù)組為空,走初始化的邏輯 // 有人可能會好奇,這里怎么會為空呢? // 大家可以去看下putAll的源碼(放在下面),源碼里面第一句調(diào)用的就是tryPresize的方法 if (tab == null || (n = tab.length) == 0) { // 進(jìn)來執(zhí)行初始化 // sc是初始化長度,初始化長度如果比計算出來的c要大的話,直接使用sc,如果沒有sc大, // 說明sc無法容納下putAll中傳入的map,使用更大的數(shù)組長度 n = (sc > c) ? sc : c; // 還是老配方,CAS將SIZECTL嘗試從sc變成-1 if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // Step1:判斷引用 // Step2:創(chuàng)建數(shù)組 // Step3:修改sizeCtl數(shù)值 if (table == tab) { Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } } } // 如果計算出來的長度c如果小于等于sc,直接退出循環(huán)結(jié)束方法 // 數(shù)組長度大于等于最大長度了,直接退出循環(huán)結(jié)束方法 // 不需要擴(kuò)容 else if (c <= sc || n >= MAXIMUM_CAPACITY) break; // 省略部分代碼 } } // putAll方法 public void putAll(Map<? extends K, ? extends V> m) { // 直接走擴(kuò)容的邏輯 tryPresize(m.size()); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) putVal(e.getKey(), e.getValue(), false); }
3、tryPreSize方法-計算擴(kuò)容戳
private final void tryPresize(int size) { int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); int sc; while ((sc = sizeCtl) >= 0) { Node<K,V>[] tab = table; // 當(dāng)前數(shù)組的長度 int n; // 判斷引用是否一致 if (tab == table) { // 計算擴(kuò)容標(biāo)識戳,根據(jù)當(dāng)前數(shù)組的長度計算一個16位的擴(kuò)容戳 // 1、保證后面的SIZECTL賦值是負(fù)數(shù) // 2、記錄當(dāng)前從什么長度開始擴(kuò)容 int rs = resizeStamp(n); // 這里存在bug // 由于我們上面(sc = sizeCtl) >= 0的判斷,sc只能大于0 // 這里永遠(yuǎn)大于等于0 if (sc < 0) { // 協(xié)助擴(kuò)容的代碼 } // 代表當(dāng)前沒有線程進(jìn)行擴(kuò)容,我是第一個擴(kuò)容的 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); } } } // 計算擴(kuò)容標(biāo)識戳 static final int resizeStamp(int n) { return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); }
4、tryPreSize方法-對sizeCtl的修改
private final void tryPresize(int size) { // sc默認(rèn)為sizeCtl while ((sc = sizeCtl) >= 0) { else if (tab == table) { // rs:擴(kuò)容戳 00000000 00000000 10000000 00011010 int rs = resizeStamp(n); if (sc < 0) { // 說明有線程正在擴(kuò)容,過來幫助擴(kuò)容 // 我們之前第一個線程擴(kuò)容的時候,將sizeCtl設(shè)置成:10000000 00011010 00000000 00000010 // 所以:sc = 10000000 00011010 00000000 00000010 Node<K,V>[] nt; // 依然有BUG // 當(dāng)前線程擴(kuò)容時,老數(shù)組長度是否和我當(dāng)前線程擴(kuò)容時的老數(shù)組長度一致 // 00000000 00000000 10000000 00011010 if ((sc >>> RESIZE_STAMP_SHIFT) != rs // 10000000 00011010 00000000 00000010 // 00000000 00000000 10000000 00011010 // 這兩個判斷都是有問題的,核心問題就應(yīng)該先將rs左移16位,再追加當(dāng)前值。 // 這兩個判斷是BUG // 判斷當(dāng)前擴(kuò)容是否已經(jīng)即將結(jié)束 || sc == rs + 1 // sc == rs << 16 + 1 BUG // 判斷當(dāng)前擴(kuò)容的線程是否達(dá)到了最大限度 || sc == rs + MAX_RESIZERS // sc == rs << 16 + MAX_RESIZERS BUG // 擴(kuò)容已經(jīng)結(jié)束了。 || (nt = nextTable) == null // 記錄遷移的索引位置,從高位往低位遷移,也代表擴(kuò)容即將結(jié)束。 || transferIndex <= 0) break; // 如果線程需要協(xié)助擴(kuò)容,首先就是對sizeCtl進(jìn)行+1操作,代表當(dāng)前要進(jìn)來一個線程協(xié)助擴(kuò)容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) // 上面的判斷沒進(jìn)去的話,nt就代表新數(shù)組 transfer(tab, nt); } // 是第一個來擴(kuò)容的線程 // 基于CAS將sizeCtl修改為:10000000 00011010 00000000 00000010 // 將擴(kuò)容戳左移16位之后,符號位是1,就代碼這個值為負(fù)數(shù) // 低16位在表示當(dāng)前正在擴(kuò)容的線程有多少個:低16位為2,代表當(dāng)前有一個線程正在擴(kuò)容 // 為什么低16位值為2時,代表有一個線程正在擴(kuò)容 // 每一個線程擴(kuò)容完畢后,會對低16位進(jìn)行-1操作,當(dāng)最后一個線程擴(kuò)容完畢后,減1的結(jié)果還是-1, // 當(dāng)值為-1時,要對老數(shù)組進(jìn)行一波掃描,查看是否有遺漏的數(shù)據(jù)沒有遷移到新數(shù)組 else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) // 調(diào)用transfer方法,并且將第二個參數(shù)設(shè)置為null,就代表是第一次來擴(kuò)容! transfer(tab, null); } } }
5、transfer方法-計算每個線程遷移的長度
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // 數(shù)組長度 int n = tab.length; // 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組 int stride; // 基于CPU的線程數(shù)來計算:NCPU = Runtime.getRuntime().availableProcessors() // MIN_TRANSFER_STRIDE = 16 // NCPU = 4 // 舉個例子:N = 1024 - 512 - 256 - 128 / 4 = 32 // 如果算出來每個線程的長度小于16的話,直接使用16 // 如果大于16的話,則使用N // 如果線程數(shù)只有1的話,直接就是原數(shù)組長度 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE){ stride = MIN_TRANSFER_STRIDE; } }
6、 transfer方法-構(gòu)建新數(shù)組并查看標(biāo)識屬性
// 以32位長度數(shù)組擴(kuò)容到64位為例子 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // 數(shù)組長度 int n = tab.length; // 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組 int stride; // 第一個進(jìn)來擴(kuò)容的線程需要把新數(shù)組構(gòu)建出來 if (nextTab == null) { try { // 將原數(shù)組長度左移一位創(chuàng)建新數(shù)組 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 賦值操作 nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; } // 將成員變量的新數(shù)組賦值volatile修飾 nextTable = nextTab; // 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾 transferIndex = n; } // 新數(shù)組長度 int nextn = nextTab.length; // 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 遷移數(shù)據(jù)時,需要用到的標(biāo)識 boolean advance = true; boolean finishing = false; }
7、transfer方法-線程領(lǐng)取遷移任務(wù)
// 以32位長度數(shù)組擴(kuò)容到64位為例子 // 數(shù)組長度 int n = tab.length; // 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組 int stride; // 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾 transferIndex = n; // 新數(shù)組長度 int nextn = nextTab.length; // 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // advance:true,代表當(dāng)前線程需要接收任務(wù),然后再執(zhí)行遷移 // 如果為false,代表已經(jīng)接收完任務(wù) boolean advance = true; // 標(biāo)識遷移是否結(jié)束 boolean finishing = false; // 開始循環(huán) for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; while (advance) { int nextIndex; int nextBound; // 第一次:這里肯定進(jìn)不去 // 主要判斷當(dāng)前任務(wù)是否執(zhí)行完畢 if (--i >= bound || finishing) advance = false; // 第一次:這里肯定也進(jìn)不去 // 判斷transferIndex是否小于等于0,代表沒有任務(wù)可領(lǐng)取,結(jié)束了。 // 在線程領(lǐng)取任務(wù)會,會對transferIndex進(jìn)行修改,修改為transferIndex - stride // 在任務(wù)都領(lǐng)取完之后,transferIndex肯定是小于等于0的,代表沒有遷移數(shù)據(jù)的任務(wù)可以領(lǐng)取 else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } // nextIndex=32 // stride=16 // nextIndex > stride ? nextIndex - stride : 0:當(dāng)前的nextIndex是否大于每個線程切割的 // 是:nextIndex - stride/否:0 // 將TRANSFERINDEX從nextIndex變成16 else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ?nextIndex - stride : 0))) { // 對bound賦值(16) bound = nextBound; // 對i賦值(31) i = nextIndex - 1; // 賦值,代表當(dāng)前領(lǐng)取任務(wù)結(jié)束 // 該線程當(dāng)前領(lǐng)取的任務(wù)是(16~31) advance = false; } } }
線程領(lǐng)取擴(kuò)容任務(wù)的流程:
8、transfer方法-擴(kuò)容是否已經(jīng)結(jié)束
// 判斷擴(kuò)容是否已經(jīng)結(jié)束! // i < 0:當(dāng)前線程沒有接收到任務(wù)! // i >= n: 遷移的索引位置,不可能大于數(shù)組的長度,不會成立 // i + n >= nextn:因為i最大值就是數(shù)組索引的最大值,不會成立 if (i < 0 || i >= n || i + n >= nextn) { int sc; // 第一次到這里:必定是false // 如果再次到這里的時候,最后一個擴(kuò)容的線程也掃描完了 if (finishing) { nextTable = null; table = nextTab; // 重新更改當(dāng)前的sizeCtl的數(shù)值:0.75N sizeCtl = (n << 1) - (n >>> 1); return; } // 到這里代表當(dāng)前線程已經(jīng)不需要去擴(kuò)容了 // 那么它要把當(dāng)前并發(fā)擴(kuò)容的線程數(shù)減一 // SIZECTL ----> sc - 1 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 判斷當(dāng)前這個線程是不是最后一個擴(kuò)容線程 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // 如果不是的話,直接返回就好了 return; // 如果是的話,當(dāng)前線程還需要將舊數(shù)據(jù)全部掃描一遍,校驗一下是不是都遷移完了 finishing = advance = true; // 將當(dāng)前的i重新變成原數(shù)組的長度 // 重新進(jìn)行一輪校驗 i = n; // recheck before commit } }
9、 transfer方法-遷移數(shù)據(jù)(鏈表)
// 如果當(dāng)前的i位置上沒有數(shù)據(jù),直接不需要遷移 else if ((f = tabAt(tab, i)) == null) // 將原來i位置上CAS標(biāo)記成fwd // fwd的Hash為Move advance = casTabAt(tab, i, null, fwd); // // 拿到當(dāng)前i位置的hash值,如果為MOVED,證明數(shù)據(jù)已經(jīng)遷移過了。 else if ((fh = f.hash) == MOVED) // 那么直接返回即可,主要是最后一個擴(kuò)容線程掃描的時候使用 advance = true; // already processed else { // 鎖住 synchronized (f) { // 再次校驗 if (tabAt(tab, i) == f) { // ln:null - lowNode Node<K,V> ln; // hn:null - highNode Node<K,V> hn; // 如果當(dāng)前的fh是正常的 if (fh >= 0) { // 求當(dāng)前的runBit // 這里只會有兩個結(jié)果:0 或者 n int runBit = fh & n; // 用當(dāng)前l(fā)astRun指向f Node<K,V> lastRun = f; // 進(jìn)行鏈表的遍歷 for (Node<K,V> p = f.next; p != null; p = p.next) { // 求每一個鏈表的p.hash & n int b = p.hash & n; // 如果當(dāng)前的b不等于上一個節(jié)點 // 需要更新下runBit的數(shù)據(jù)和lastRun的指針 if (b != runBit) { runBit = b; lastRun = p; } } // 如果最后runBit=0的話 // lastRun賦值給ln if (runBit == 0) { ln = lastRun; hn = null; } // 如果最后runBit=0的話 // lastRun賦值給hn else { hn = lastRun; ln = null; } // 從數(shù)組頭節(jié)點開始一直遍歷到lastRun位置 for (Node<K,V> p = f; p != lastRun; p = p.next) { // hash/key/value int ph = p.hash; K pk = p.key; V pv = p.val; // 如果當(dāng)前的是0,則掛到ln下面 if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); // 如果當(dāng)前的是1,則掛到hn下面 else hn = new Node<K,V>(ph, pk, pv, hn); } // 將新數(shù)組i位置設(shè)置成ln setTabAt(nextTab, i, ln); // 將新數(shù)組i+N位置設(shè)置成hn setTabAt(nextTab, i + n, hn); // 將原來數(shù)組的i位置設(shè)置成fwd,代表遷移完畢 setTabAt(tab, i, fwd); // 將advance設(shè)置成true,保證進(jìn)行上面的while循環(huán) // 執(zhí)行上面的i--,進(jìn)行下一節(jié)點的遷移計劃 advance = true; } } } } }
9.1 LastRun機(jī)制
假如當(dāng)前我需要將數(shù)組從 16 擴(kuò)容至 32,我們看下原來的結(jié)構(gòu):
當(dāng)我們進(jìn)行下面這一步時:
int runBit = fh & n; for (Node<K,V> p = f.next; p != null; p = p.next) { // 求每一個鏈表的p.hash & n int b = p.hash & n; // 如果當(dāng)前的b不等于上一個節(jié)點 // 需要更新下runBit的數(shù)據(jù)和lastRun的指針 if (b != runBit) { runBit = b; lastRun = p; } }
圖片如下:紫色代表 0
,粉色代表 1
在遷移之前,我們要掌握一個知識,由于我們的數(shù)組是 2
倍擴(kuò)容,所以原始數(shù)據(jù)的數(shù)據(jù)一定會落在新數(shù)組的 2
和 N + 2
的位置
最后將我們的數(shù)組從原始數(shù)組直接遷移過來,由于 lastRun
之后位置的數(shù)據(jù)都是一樣的hash,所以直接全量遷移即可,不需要挨個遍歷,這也是實施 lastRun
的原因,減少時間復(fù)雜度
最終遷移效果如上,遷移完畢,減少 i
的下標(biāo),繼續(xù)遷移下一個數(shù)組的位置。
10、協(xié)助擴(kuò)容
// 如果當(dāng)前的hash是正在擴(kuò)容 if ((fh = f.hash) == MOVED){ // 進(jìn)行協(xié)作式擴(kuò)容 tab = helpTransfer(tab, f); } final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; // 第一個判斷:老數(shù)組不為null // 第二個判斷:新數(shù)組不為null (將新數(shù)組賦值給nextTab) if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { // 拿到擴(kuò)容戳 int rs = resizeStamp(tab.length); // 第一個判斷:fwd中的新數(shù)組,和當(dāng)前正在擴(kuò)容的新數(shù)組是否相等。 // 相等:可以協(xié)助擴(kuò)容。不相等:要么擴(kuò)容結(jié)束,要么開啟了新的擴(kuò)容 // 第二個判斷:老數(shù)組是否改變了。 相等:可以協(xié)助擴(kuò)容。不相等:擴(kuò)容結(jié)束了 // 第三個判斷:如果正在擴(kuò)容,sizeCtl肯定為負(fù)數(shù),并且給sc賦值 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { // 判斷擴(kuò)容戳是否一致 // 看下transferIndex是否小于0,如果小于的話,說明擴(kuò)容的任務(wù)已經(jīng)被領(lǐng)完了 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; // 如果還有任務(wù)的話,CAS將當(dāng)前的SIZECTL加一,協(xié)助擴(kuò)容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 協(xié)助擴(kuò)容的代碼,上面剛剛分析過 transfer(tab, nextTab); break; } } return nextTab; } return table; }
三、流程圖
以上就是Java面試常考之ConcurrentHashMap多線程擴(kuò)容機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于Java ConcurrentHashMap的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java基礎(chǔ)強(qiáng)化訓(xùn)練輸入錯誤即結(jié)束進(jìn)程
本文主要介紹了Java編程的基礎(chǔ)知識強(qiáng)化應(yīng)用,文中實例涉及到了許多基礎(chǔ)知識,new對象,控制臺輸入,if語句等。很實用,需要的朋友可以參考下2017-09-09Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn)
這篇文章主要介紹了Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09Java中抽象類和接口的區(qū)別_動力節(jié)點Java學(xué)院整理
java抽象類和接口最本質(zhì)的區(qū)別是接口里不能實現(xiàn)方法--接口中的方法全是抽象方法。抽象類中可實現(xiàn)方法--抽象類中的方法可以不是抽象方法,下文給大家簡單介紹下,需要的的朋友參考下2017-04-04Java Mybatis框架多表操作與注解開發(fā)詳解分析
MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO為數(shù)據(jù)庫中的記錄2021-10-10java學(xué)習(xí)筆記之DBUtils工具包詳解
下面小編就為大家分享一篇java學(xué)習(xí)筆記之DBUtils工具包詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01解決java執(zhí)行cmd命令調(diào)用ffmpeg報錯Concat error - No such filter ''[0,0]
這篇文章主要介紹了java執(zhí)行cmd命令,調(diào)用ffmpeg報錯Concat error - No such filter '[0,0]'解決方法,本文通過截圖實例代碼說明給大家介紹的非常詳細(xì),對大家的工作或?qū)W習(xí)有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03Java如何獲取數(shù)組和字符串的長度(length還是length())
這篇文章主要介紹了Java如何獲取數(shù)組和字符串的長度(length還是length()),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12