淺析Java中ConcurrentHashMap的存儲(chǔ)流程
一、引言
ConcurrentHashMap技術(shù)在互聯(lián)網(wǎng)技術(shù)使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在ConcurrentHashMap技術(shù)的使用和原理方面對(duì)小伙伴們進(jìn)行 360° 的刁難。
作為一個(gè)在互聯(lián)網(wǎng)公司面一次拿一次 Offer 的面霸,打敗了無(wú)數(shù)競(jìng)爭(zhēng)對(duì)手,每次都只能看到無(wú)數(shù)落寞的身影失望的離開(kāi),略感愧疚(請(qǐng)?jiān)试S我使用一下夸張的修辭手法)。
于是在一個(gè)寂寞難耐的夜晚,暖男我痛定思痛,決定開(kāi)始寫(xiě) 《吊打面試官》 系列,希望能幫助各位讀者以后面試勢(shì)如破竹,對(duì)面試官進(jìn)行 360° 的反擊,吊打問(wèn)你的面試官,讓一同面試的同僚瞠目結(jié)舌,瘋狂收割大廠 Offer!
雖然現(xiàn)在是互聯(lián)網(wǎng)寒冬,但乾坤未定,你我皆是黑馬
二、使用
我們經(jīng)常在線程安全的場(chǎng)景下使用 ConcurrentHashMap
,基本使用如下:
public class ConcurrentHashMapTest { public static void main(String[] args) { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); map.put("test1", "1"); map.put("test2", "2"); map.put("test3", "3"); map.remove("test1"); System.out.println(map.get("test1")); System.out.println(map.get("test2")); } }
使用的話,我相信大部分的讀者應(yīng)該都會(huì)的,這里小黃也不多介紹了,我們直接進(jìn)入正題
三、源碼
1、初始化
還是我們的老樣子,從初始化開(kāi)始聊源碼
如果我們初始化時(shí)不攜帶入?yún)⒌脑?,初始化方法如下?strong>可以看到,基本沒(méi)有什么東西
public ConcurrentHashMap() {}
如果你攜帶了入?yún)⒌脑?,初始化方法如下?/p>
public ConcurrentHashMap(int initialCapacity) { // 假如哥們傳進(jìn)來(lái)入?yún)⑿∮? if (initialCapacity < 0) // 直接拋出異常,說(shuō)明哥們?cè)诟阈? throw new IllegalArgumentException(); // 用傳進(jìn)來(lái)的數(shù)值與 MAXIMUM_CAPACITY >>> 1 進(jìn)行對(duì)比 // 若大于則使用MAXIMUM_CAPACITY // 小于則使用距離initialCapacity最近的2次冪 int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; } // 根據(jù)傳進(jìn)的C的數(shù)值,找到其距離最近的2次冪 private static final int tableSizeFor(int c) { int n = c - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
2、存儲(chǔ)操作
public V put(K key, V value) { // 在調(diào)用put方法時(shí),會(huì)調(diào)用putVal,第三個(gè)參數(shù)默認(rèn)傳遞為false // 在調(diào)用putIfAbsent時(shí),會(huì)調(diào)用putVal方法,第三個(gè)參數(shù)傳遞的為true // 如果傳遞為false,代表key一致時(shí),直接覆蓋數(shù)據(jù) // 如果傳遞為true,代表key一致時(shí),什么都不做,key不存在,正常添加(Redis,setnx) return putVal(key, value, false); }
2.1 計(jì)算索引下標(biāo)
final V putVal(K key, V value, boolean onlyIfAbsent) { // 如果當(dāng)前的key或者value是空的話,直接拋出異常 if (key == null || value == null){ throw new NullPointerException(); } // 獲取其下標(biāo) int hash = spread(key.hashCode()); int binCount = 0; } // 作用:用高16位與低16位進(jìn)行^運(yùn)算,讓高位的數(shù)值可以進(jìn)行計(jì)算 // 為什么原來(lái)的高位沒(méi)有辦法計(jì)算呢? // 我們后面的 (n - 1) & hash 的數(shù)據(jù),&數(shù)據(jù)如下: // 00000000 00000000 00000000 01010101 // 0000000 00000000 00000000 00011111 // 我們這里看到,如果高16位不與低16位^運(yùn)算的話,那么基本我們的高位永遠(yuǎn)也參加不了計(jì)算 // 為什么需要&HASH_BITS: // 保證最終的結(jié)果大于0,因?yàn)槿绻Y(jié)果小于0的話,代表不同的意義: // static final int MOVED = -1; // 代表當(dāng)前hash位置的數(shù)據(jù)正在擴(kuò)容! // static final int TREEBIN = -2; // 代表當(dāng)前hash位置下掛載的是一個(gè)紅黑樹(shù) // static final int RESERVED = -3; // 預(yù)留當(dāng)前索引位置…… static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
2.2 初始化數(shù)組
// tab指向table,標(biāo)準(zhǔn)的Doug Lea寫(xiě)法 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 如果當(dāng)前的數(shù)組為空或者他的數(shù)組長(zhǎng)度為0 // 則進(jìn)行初始化 if (tab == null || (n = tab.length) == 0){ tab = initTable(); } } // sizeCtl:是數(shù)組在初始化和擴(kuò)容操作時(shí)的一個(gè)控制變量 // -1:代表當(dāng)前數(shù)組正在初始化 // 小于-1:低16位代表當(dāng)前數(shù)組正在擴(kuò)容的線程個(gè)數(shù)(如果1個(gè)線程擴(kuò)容,值為-2,如果2個(gè)線程擴(kuò)容,值為-3) // 0:代表數(shù)組還沒(méi)初始化 // 大于0:代表當(dāng)前數(shù)組的擴(kuò)容閾值,或者是當(dāng)前數(shù)組的初始化大小 private final Node<K,V>[] initTable() { // 經(jīng)典引用 Node<K,V>[] tab; int sc; // 當(dāng)前的初始化沒(méi)有完成時(shí),會(huì)一直進(jìn)行該while循環(huán) while ((tab = table) == null || tab.length == 0) { // 如果小于0,代表當(dāng)前數(shù)組正在擴(kuò)容或者初始化 // 當(dāng)前線程等待一下 if ((sc = sizeCtl) < 0) Thread.yield(); // 嘗試將SIZECTL從SC更改為-1 // CAS修改,線程安全,保證只有一個(gè)線程執(zhí)行數(shù)組初始化 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 更改成功,再次判斷一下(參考DCL) // 防止下面sizeCtl = sc剛賦值完,正好有線程走到這一步,不做限制的話就會(huì)重新初始化了 if ((tab = table) == null || tab.length == 0) { // 判斷當(dāng)前的sc是否大于0(sc=SIZECTL) // 大于0:n = sc // 小于等于0:n = DEFAULT_CAPACITY // 一般我們只要不傳入入?yún)?這里基本走DEFAULT_CAPACITY的擴(kuò)容 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 擴(kuò)容即可 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // table、tab賦值 table = tab = nt; // 這里有點(diǎn)意思 // 將sc賦值成(n - 1/4n) = 0.75N // 這里0.75是不是很熟悉,負(fù)載因子 sc = n - (n >>> 2); } } finally { // 將上面的擴(kuò)容閾值賦予sizeCtl sizeCtl = sc; } // 結(jié)束循環(huán) break; } } return tab; }
2.3 將數(shù)據(jù)插入到數(shù)組
// 如果當(dāng)前數(shù)組該下標(biāo)沒(méi)有數(shù)據(jù),直接插入即可 if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // CAS將當(dāng)前的hash、key、value組裝成一個(gè)Node,插入當(dāng)前數(shù)組i位置 // 插入成功結(jié)束即可 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))){ break; } } // 基于我們上面說(shuō)的(n - 1) & hash算出下標(biāo) // 返回當(dāng)前數(shù)組下該下標(biāo)的數(shù)據(jù) static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } static class Node<K,V> implements Map.Entry<K,V> { // 當(dāng)前的hash值 final int hash; // key final K key; // value volatile V val; // 下一個(gè)節(jié)點(diǎn)(用來(lái)連鏈表的) volatile Node<K,V> next; }
2.4 擴(kuò)容(后面聊)
- 這部分后面聊
// 判斷當(dāng)前位置數(shù)據(jù)是否正在擴(kuò)容…… if ((fh = f.hash) == MOVED) // 如果在擴(kuò)容,則當(dāng)前線程幫助其擴(kuò)容 tab = helpTransfer(tab, f);
2.5 將數(shù)據(jù)插入到鏈表
else { V oldVal = null; // 鎖當(dāng)前的數(shù)組下標(biāo)i的數(shù)組塊 synchronized (f) { // 看一下當(dāng)前數(shù)組的i位置是不是等于f // 相當(dāng)于再次校驗(yàn)一次(DCL) if (tabAt(tab, i) == f) { // static final int MOVED = -1; // 代表當(dāng)前hash位置的數(shù)據(jù)正在擴(kuò)容! // static final int TREEBIN = -2; // 代表當(dāng)前hash位置下掛載的是一個(gè)紅黑樹(shù) // static final int RESERVED = -3; // 預(yù)留當(dāng)前索引位置…… // fh = f.hash // 判斷下當(dāng)前的fh是否大于0,也就是不是上面三種情況 if (fh >= 0) { // 記錄當(dāng)前鏈表下面掛了幾個(gè) binCount = 1; // 獲取當(dāng)前的數(shù)組節(jié)點(diǎn),沒(méi)循環(huán)一次binCount加一 for (Node<K,V> e = f;; ++binCount) { K ek; // 如果當(dāng)前數(shù)組下標(biāo)的hash和我們?nèi)雲(yún)⒌膆ash一樣,代表重復(fù)數(shù)據(jù) if (e.hash == hash && // 如果當(dāng)前的key也等于原始的key // 或者是根據(jù)equals判斷出來(lái)的相等(因?yàn)橛幸恍┛赡苤貙?xiě)了equals方法) ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 將當(dāng)前老的數(shù)據(jù)賦值給oldVal oldVal = e.val; // 這里的onlyIfAbsent我們之前也聊過(guò) // 如果傳遞為false,代表key一致時(shí),直接覆蓋數(shù)據(jù) // 如果傳遞為true,代表key一致時(shí),什么都不做,key不存在,正常添加(Redis,setnx) if (!onlyIfAbsent) // 覆蓋數(shù)據(jù) e.val = value; break; } // 這里就不是相同的數(shù)據(jù)了,需要掛鏈表下面了 // 先獲取數(shù)組最上面的數(shù)據(jù) Node<K,V> pred = e; // 判斷下當(dāng)前的下一個(gè)數(shù)據(jù)是不是空指針 // 不是空指針的話,繼續(xù)指向下一個(gè)指針 if ((e = e.next) == null) { // 直到最后的時(shí)候,創(chuàng)建一個(gè)節(jié)點(diǎn)掛上去 pred.next = new Node<K,V>(hash, key,value, null); break; } } } }
2.6 將數(shù)據(jù)插入到紅黑樹(shù)
// 如果上面不成立的話,也就是當(dāng)前的數(shù)組下面是一個(gè)紅黑樹(shù) // 需要將當(dāng)前的數(shù)據(jù)放到紅黑樹(shù)里面 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; // 將當(dāng)前數(shù)據(jù)放入到紅黑樹(shù)中 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { // 記錄一下老數(shù)據(jù) oldVal = p.val; // 這里的onlyIfAbsent我們之前也聊過(guò) // 如果傳遞為false,代表key一致時(shí),直接覆蓋數(shù)據(jù) // 如果傳遞為true,代表key一致時(shí),什么都不做,key不存在,正常添加(Redis,setnx) if (!onlyIfAbsent) // 覆蓋數(shù)據(jù) p.val = value; } }
2.7 鏈表轉(zhuǎn)紅黑樹(shù)
// 如果當(dāng)前的鏈表上的數(shù)據(jù)不等于0 if (binCount != 0) { // 當(dāng)前列表下的數(shù)據(jù)長(zhǎng)度大于8 // 這里需要注意,大于8的話并不是立即轉(zhuǎn)成紅黑樹(shù),還需要判斷當(dāng)前數(shù)組的長(zhǎng)度 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } static final int TREEIFY_THRESHOLD = 8; static final int MIN_TREEIFY_CAPACITY = 64; private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; // 如果當(dāng)前的數(shù)組不為空 if (tab != null) { // 如果當(dāng)前的數(shù)組長(zhǎng)度小于64,則沒(méi)必要轉(zhuǎn)成紅黑樹(shù) // 直接擴(kuò)容即可 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); // 后面的是轉(zhuǎn)成紅黑樹(shù)的代碼 // 我們下一章在分析 else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { synchronized (b) { if (tabAt(tab, index) == b) { TreeNode<K,V> hd = null, tl = null; for (Node<K,V> e = b; e != null; e = e.next) { TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null); if ((p.prev = tl) == null){ hd = p; } else{ tl.next = p; } tl = p; } setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } } }
四、流程圖
1、初始化階段
2、存儲(chǔ)階段
五、總結(jié)
魯迅先生曾說(shuō):獨(dú)行難,眾行易,和志同道合的人一起進(jìn)步。彼此毫無(wú)保留的分享經(jīng)驗(yàn),才是對(duì)抗互聯(lián)網(wǎng)寒冬的最佳選擇。
其實(shí)很多時(shí)候,并不是我們不夠努力,很可能就是自己努力的方向不對(duì),如果有一個(gè)人能稍微指點(diǎn)你一下,你真的可能會(huì)少走幾年彎路。
以上就是淺析Java中ConcurrentHashMap的存儲(chǔ)流程的詳細(xì)內(nèi)容,更多關(guān)于ConcurrentHashMap存儲(chǔ)流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Java的ConcurrentHashMap原理深入分析
- Java?ConcurrentHashMap實(shí)現(xiàn)線程安全的代碼示例
- Java面試??贾瓹oncurrentHashMap多線程擴(kuò)容機(jī)制詳解
- Java源碼重讀之ConcurrentHashMap詳解
- Java?ConcurrentHashMap的源碼分析詳解
- Java集合ConcurrentHashMap詳解
- java并發(fā)容器ConcurrentHashMap深入分析
- Java HashTable的原理與實(shí)現(xiàn)
- Java中HashMap和Hashtable的區(qū)別小結(jié)
- 一文帶你全面了解Java?Hashtable
- Java中Hashtable集合的常用方法詳解
- 詳解Java中的HashTable
- Java容器HashMap與HashTable詳解
- java HashMap和HashTable的區(qū)別詳解
- java面試題——詳解HashMap和Hashtable 的區(qū)別
- Java中HashMap和Hashtable的區(qū)別淺析
- Java中ConcurrentHashMap和Hashtable的區(qū)別
相關(guān)文章
SpringBoot多種自定義錯(cuò)誤頁(yè)面方式小結(jié)
這篇文章主要介紹了SpringBoot多種自定義錯(cuò)誤頁(yè)面方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11

SpringBoot使用hutool-captcha實(shí)現(xiàn)驗(yàn)證碼生成與驗(yàn)證

try catch finally的執(zhí)行順序深入分析

Java線程池ThreadPoolExecutor原理及使用實(shí)例