淺談Java源碼ConcurrentHashMap
一、記錄形式
打算直接把過程寫在源碼中,會按序進(jìn)行注釋,查閱的時候可以按序號只看注釋部分
二、ConcurrentHashMap
直接模擬該類的使用過程,從而一步步看其怎么運作的吧,當(dāng)然最好還是帶著問題一遍思考一遍總結(jié)會比較好,我閱讀源碼的時候帶著以下幾個問題
- 并發(fā)體現(xiàn)在哪里?怎么保證線程安全的
- 怎么擴容的?擴容是怎么保證線程安全的?
- 怎么put的?put是怎么保證線程安全的?
- 用了哪些鎖?這些鎖的作用是什么?
- 需要留意哪些關(guān)鍵點?
我們最簡單地使用方法是怎么樣的?
- new一個ConcurrentHashMap對象
- 調(diào)用put方法放入k-v對
- 調(diào)用get方法獲取k-v對
那么很顯然,考慮只有在進(jìn)行修改與更新時需要考慮并發(fā),所以我關(guān)注的重點在put方法與擴容上
首先new一個對象時,我們傳參入一個size,調(diào)用其只有一個參數(shù)的構(gòu)造器
public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); // 1、判斷傳入的大小是否超過最大值的一半,若超過則取最大值 // 2、若沒超過,則調(diào)用tableSizeFor // 3、tableSizeFor可以讓size為2的倍數(shù),為什么要是2的倍數(shù)呢?因為對hash取模的時候 // 用的是位運算,只有size為2的倍數(shù)才能這么做 int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; }
// 1、判斷傳入的大小是否超過最大值的一半,若超過則取最大值 // 2、若沒超過,則調(diào)用tableSizeFor // 3、tableSizeFor讓size為2的倍數(shù),為什么要是2的倍數(shù)呢?因為對hash取模的時候 // 用的是位運算,只有size為2的倍數(shù)才能這么做
注意,此時并沒有創(chuàng)建map數(shù)據(jù)結(jié)構(gòu),所以ConcurrentHashMap是懶惰創(chuàng)建的
接著我們調(diào)用put方法放入數(shù)據(jù),put方法調(diào)的putVal
final V putVal(K key, V value, boolean onlyIfAbsent) { // 1、k-v都不能為空,不然拋異常 if (key == null || value == null) throw new NullPointerException(); // 2、獲取key的hashcode的hash值 int hash = spread(key.hashCode()); // 3、使用binCount來統(tǒng)計鏈表有多少個元素的,便于后面判斷是否需要變成紅黑樹 int binCount = 0; // 4、創(chuàng)建tab臨時變量,并賦值為table,由于還沒初始化,值為null // 這里注意了,table的類型是Node數(shù)組,這個Node其實就是Map.Entry<K,V> for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 5、判斷tab為空后才進(jìn)行初始化,初始化完成后重新進(jìn)入循環(huán) if (tab == null || (n = tab.length) == 0) tab = initTable(); // 6、此時已經(jīng)初始化好了,可以開始插入數(shù)據(jù)。 // 這里用tabAt(tab,i)獲取tab的第i個下標(biāo)上的鏈表指針 // 注意了,(n-1)& hash其實就是hash%n,只有n為2的次方才能這么做 // 位運算可以提升效率 // 7、f就是獲取到的第i個位置的鏈表頭指針 // 如果為null說明什么都沒有,可以嘗試插入元素 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 8、考慮插入那就要考慮并發(fā)了,casTab表示用cas方法進(jìn)行插入 // 插入一個新的Node結(jié)點,這個是能夠保證線程安全的 // 我們知道保證線程安全除了用cas之外還不夠,還需要保證操作對象的可見性 // 在這里是對tab進(jìn)行操作,tab在前面用table賦值,而table是加了volatile的 // 所以沒毛病哈 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 9、如果f不為空,并且f.hash==MOVED(-1),說明當(dāng)前這個位置正在被移動 // 說明有線程在擴容,那么當(dāng)然不能這時候還去插入了,這里調(diào)用helpTransfer去幫助擴容 // 注意了,這意味著擴容時是一個一個位置來移動的,每移動一個就將f.hash改成MOVED // 也就意味著如果當(dāng)前線程想要操作的位置還沒有被移動時是可以操作的,這使得并發(fā)度更高了 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 10、如果f.hash表示沒有被移動,且f不為null說明可以這個位置已經(jīng)有東西了 // 需要對其遍歷,并找到合適的位置插入 else { V oldVal = null; // 11、由于要進(jìn)行插入了,所以得加鎖,注意了哈,這里用的synchronized // 并且鎖住的是當(dāng)前位置對象(不一定是鏈表也可能是樹) // 意味著除了當(dāng)前線程,其他線程都不準(zhǔn)操作了哈 // 如果這時候正在擴容的線程擴到這里了,會被阻塞的哈 synchronized (f) { // 確定f為起點 if (tabAt(tab, i) == f) { // 12、判斷f.hash是大于0,大于0表示當(dāng)前這個東西是鏈表 // 下面執(zhí)行鏈表的更新操作 if (fh >= 0) { binCount = 1; // 13、接著就是具體的遍歷鏈表,查找是否對應(yīng)值是否存在 // 如果遍歷完都找不到,那么就在尾部插入新的結(jié)點 // 注意了哈這就是尾插 // 14、同時,每遍歷一個結(jié)點還要累加binCount // 即統(tǒng)計一下當(dāng)前鏈表個數(shù),便于后面轉(zhuǎn)紅黑樹判斷 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 13、如果f對應(yīng)的是樹結(jié)構(gòu),那就執(zhí)行樹的更新方法 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 14、前面說了哈,binCount就是鏈表元素個數(shù),接著就判斷是否大于閾值 // 大于則轉(zhuǎn)樹,可以看這個閾值TREEIFY_THRESHOLD=8 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 15、addCount是讓size+1的 // 這里要注意,加法也是分多步的,需要先get才能+,因此為了保證線程安全也是需要用cas的 // 這里加size的方式并不是直接往size上加,因為size是經(jīng)常修改的,如果經(jīng)常訪問的話效率很低 // 那么做法和LongAdder這一原子累加器類似,用一個CountCell數(shù)組,每個線程只操作數(shù)組中的 // 某一個Cell,最后匯總即可 addCount(1L, binCount); return null; }
總結(jié)一下put的過程
1.先判斷map是否創(chuàng)建,沒創(chuàng)建則先創(chuàng)建,結(jié)構(gòu)為 Node<K,V>[ ] extend Map.Entry
2.接著找key應(yīng)該放在哪個位置 i
3.判斷該位置是否為空,為空則使用CAS插入一個新的Node
4.不為空則判斷當(dāng)前位置狀態(tài)是否為MOVED,是則說明當(dāng)前位置正在被其他線程改動,當(dāng)前線程需要幫助完成移動
5.不為空且不為MOVED,則判斷是鏈表還是樹,分別執(zhí)行對應(yīng)的更新方法
6.如果為鏈表
- 先對鏈表上鎖,用syn
- 則遍歷并查找是否已存在
- 找到最后都不存在則直接尾插
- 同時統(tǒng)計鏈表上的元素
7.判斷鏈表元素是否超過變成樹的閾值 8 ,超過則直接變成樹,變成樹也是加syn
8.使用addCount更新size,更新方式類似LongAdder
三、關(guān)鍵點
- 懶惰加載map
- 對當(dāng)前位置操作之前需要判斷當(dāng)前位置的存儲的內(nèi)容是否被其他線程移動了,如果被移動則先去幫助完成移動
執(zhí)行擴容移動的線程是挨個移動每個位置的鏈表,移動前會先將當(dāng)前位置的狀態(tài)用CAS改成MOVED
注意了這個是提升并發(fā)度的關(guān)鍵所在
因為插入和移動(擴容)的粒度是細(xì)化到每個位置的鏈表上
意味著擴容和插入可以同時進(jìn)行
意味著插入時上鎖后,擴容線程執(zhí)行到該位置需要阻塞
而不是直接整個map都鎖住
- 當(dāng)前位置沒內(nèi)容時,通過CAS插入新Node,而操作鏈表時用的是syn
如果面試問到ConcurrentHashMap中用了什么鎖就心中有數(shù)了
- 更新size的時候用的是LongAdder類似的方法
有一個CountCell數(shù)組,每個線程更新后,對數(shù)組中的某個Cell+1
如果沒有競爭則只有一個baseCell,對其+1
統(tǒng)計size時匯總即可
再細(xì)化一下前面的流程
思考初始化map的時候怎么保證線程安全?防止多個線程同時初始化?
答案在initTable方法中
- 可以看到,sizeCtl如果是負(fù)數(shù)說明正在擴容或者初始化
- 因此當(dāng)需要初始化時,就去CAS地改變sizeCtl,將其變?yōu)樨?fù)數(shù)
- 哪個線程CAS成功,則可以執(zhí)行初始化,這就保證了線程安全
- 可以再去看看,sizeCtl是volatile修飾的哈
- 并且SIZECTL是sizeCtl的offset,這些都是原子類類似的東西了
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
get方法就不說了,因為不涉及并發(fā),就是查找而已
感覺差不多了,把put方法搞清楚了,ConcurrentHashMap怎么解決線程安全的也清楚了,并且并發(fā)關(guān)鍵點在哪也清楚了
到此這篇關(guān)于淺談Java源碼ConcurrentHashMap的文章就介紹到這了,更多相關(guān)Java ConcurrentHashMap內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用Java對PDF文件進(jìn)行電子簽章的實戰(zhàn)過程
隨著電子賬單、回單、通知、合同的流行,電子文檔的可信度變得非常重要,為防止非法篡改,確保文檔的權(quán)威性,我們可以對PDF進(jìn)行電子簽章,這篇文章主要給大家介紹了關(guān)于如何利用Java對PDF文件進(jìn)行電子簽章的相關(guān)資料,需要的朋友可以參考下2021-07-07Java實現(xiàn)ftp文件上傳下載解決慢中文亂碼多個文件下載等問題
這篇文章主要介紹了Java實現(xiàn)ftp文件上傳下載解決慢中文亂碼多個文件下載等問題的相關(guān)資料,非常不錯具有參考借鑒價值,需要的朋友可以參考下2016-10-10java~springboot~ibatis數(shù)組in查詢的實現(xiàn)方法
這篇文章主要介紹了java~springboot~ibatis數(shù)組in查詢的實現(xiàn)方法,需要的朋友可以參考下2018-09-09Springboot中使用Filter實現(xiàn)Header認(rèn)證詳解
這篇文章主要介紹了Springboot中使用Filter實現(xiàn)Header認(rèn)證詳解,當(dāng)在?web.xml?注冊了一個?Filter?來對某個?Servlet?程序進(jìn)行攔截處理時,它可以決定是否將請求繼續(xù)傳遞給?Servlet?程序,以及對請求和響應(yīng)消息是否進(jìn)行修改,需要的朋友可以參考下2023-08-08SpringBoot如何獲取application.properties中自定義的值
這篇文章主要介紹了SpringBoot獲取application.properties中的自定義的值,目錄結(jié)構(gòu)文件代碼給大家列舉的非常詳細(xì),需要的朋友可以參考下2021-09-09