Java中的ConcurrentHashMap集合源碼解析
前言
在并發(fā)環(huán)境下,HashMap會出現(xiàn)線程安全問題,HashMap的擴容操作會出現(xiàn)閉環(huán)現(xiàn)象,當調用get方法時,會出現(xiàn)死循環(huán)。
所以,JDK給我們提供了另一個線程安全容器,ConcurrentHashMap。
在本章中我們來詳細探討JDK 1.8中ConcurrentHashMap的核心方法,且為什么是線程安全的以及和JDK 1.8之前的又有何區(qū)別。
ConcurrentHashMap底層容器和HashMap相同,同樣是Node數組+鏈表+紅黑樹,不同的是在原來的基礎之上使用了Synchronized+CAS來保證線程安全,下面我們來進行源碼分析。
put
public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { //從這我們可以看出ConcurrentHashMap的key和value不能為null if (key == null || value == null) throw new NullPointerException(); //得到key的hash值 int hash = spread(key.hashCode()); //這是用來記錄鏈表的長度 int binCount = 0; //table:核心的Node數組。 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //如果數組為空,則進行數組的初始化。這里相當于懶漢模式,使用的時候才去初始化 if (tab == null || (n = tab.length) == 0) //進行數組的初始化 tab = initTable(); //根據key的hash計算出該key在Node數組中的位置,并判斷這個位置是否為null else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //當前的數組位置為null,則使用CAS插入一個新的Node if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //表示正在進行擴容操作 涉及了ForwardingNode 這個特殊的Node, //在擴容時會進行創(chuàng)建,且固定傳入的hash值為 -1 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); //到了這里表示,出現(xiàn)了hash沖突,key在Node數組中的索引位置不為null。 //需進行鏈表或紅黑樹的插入操作。 else { V oldVal = null; //這個 f 存放是 根據key在數組中找到的Node,相當于紅黑樹或鏈表的頭結點,并進行加鎖 synchronized (f) { //這里再進行一次判斷,保證f沒被其它線程修改 if (tabAt(tab, i) == f) { //如果是鏈表 if (fh >= 0) { //統(tǒng)計鏈表的長度 binCount = 1; //對f這個Node進行累加鏈表的長度,并遍歷鏈表 for (Node<K,V> e = f;; ++binCount) { K ek; //如果存在相同的value,則覆蓋 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; //遍歷到最后一個Node,插入一個新的Node到鏈表的尾部 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } //如果是紅黑樹 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; //使用紅黑樹的方式進行Node的插入 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { //判斷鏈表的長度是否大于TREEIFY_THRESHOLD,是則轉紅黑樹 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //統(tǒng)計整個的數組長度,并判斷是否需要擴容 addCount(1L, binCount); return null; }
好了,以上就是大致的分析內容,但還有許多步驟沒有展開代碼詳細說明,如初始化、鏈表轉紅黑樹及擴容等,其中擴容步驟非常復雜,有機會我會單獨寫一篇博客詳細介紹,在這就不多說了。我們接下來介紹較為簡單的初始化及get方法。
初始化數組
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { //表示已經有線程在進行初始化操作,讓出CPU的執(zhí)行權 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //把sc賦值為 -1,表示當前線程開始執(zhí)行初始化操作 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { //獲取數組的初始長度,默認為16 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 參數 第一個if判斷需要用到 sizeCtl = sc; } break; } } return tab; }
get
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //計算key的hash int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && //根據hash值做運算獲取數組對應的Node(相當于頭結點) (e = tabAt(tab, (n - 1) & h)) != null) { //根據hash和equals判斷該Node的key是否和get的key相等,是則返回value if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //這里判斷是否正在擴容 或者 該節(jié)點為紅黑樹 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; //到了這一步表示該結構為鏈表,遍歷鏈表,返回value while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
好了,核心的源碼部分就分析到這里,我們在來看看JDK 1.8之前的ConcurrentHashMap大致是怎么實現(xiàn)的,區(qū)別相當大。
1.8之前的ConcurrentHashMap是采用了ReentrantLock+Segment+HashEntry的結構
整體就是由一個個 Segment 組成,Segment中包含了HashEntry數組,HashEntry數組就是1.8的那個table,只不過它這里是多個。
其中Segment在實現(xiàn)上繼承了ReentrantLock,這樣就自帶了鎖的功能,它在進行put的時候只會鎖住一個Segment,所以理論上,最多可以同時支持 Segment 個數的線程并發(fā)寫,只要它們的操作分別分布在不同的 Segment 上。get的時候也是先找到一個Segment,然后在根據Hash值找到數組中的值。
至于為什么JDK在之后使用Synchronized來保證線程安全,是因為在JDK在版本迭代中一直在對Synchronized進行優(yōu)化,使得Synchronized關鍵字在某些場景下已經不比ReentrantLock效率慢,甚至更快。
到此這篇關于Java中的ConcurrentHashMap集合源碼解析的文章就介紹到這了,更多相關ConcurrentHashMap集合源碼內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java中以DES的方式實現(xiàn)對稱加密并提供密鑰的實例
這篇文章主要介紹了java中以DES的方式實現(xiàn)對稱加密并提供密鑰的實例的相關資料,這里提供實例幫助大家學習理解這部分知識,需要的朋友可以參考下2017-08-08Spring Boot 整合 ShedLock 處理定時任務重復執(zhí)行的問題小結
ShedLock是解決分布式系統(tǒng)中定時任務重復執(zhí)行問題的Java庫,通過在數據庫中加鎖,確保只有一個節(jié)點在指定時間執(zhí)行任務,它與SpringScheduler、Quartz等框架結合使用,本文介紹Spring Boot 整合 ShedLock 處理定時任務重復執(zhí)行的問題,感興趣的朋友一起看看吧2025-02-02Java實戰(zhàn)之火車票預訂系統(tǒng)的實現(xiàn)
這篇文章主要介紹了利用Java實現(xiàn)的火車票預訂系統(tǒng),文中用到了JSP?、Servlert、JQuery、Ajax?等技術,文中示例代碼講解詳細,需要的可以參考一下2022-02-02springboot使用yml文件配置多環(huán)境方式(dev、test、prod)
這篇文章主要介紹了springboot使用yml文件配置多環(huán)境方式(dev、test、prod),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09