Java中的ConcurrentHashMap集合源碼解析
前言
在并發(fā)環(huán)境下,HashMap會(huì)出現(xiàn)線程安全問(wèn)題,HashMap的擴(kuò)容操作會(huì)出現(xiàn)閉環(huán)現(xiàn)象,當(dāng)調(diào)用get方法時(shí),會(huì)出現(xiàn)死循環(huán)。
所以,JDK給我們提供了另一個(gè)線程安全容器,ConcurrentHashMap。
在本章中我們來(lái)詳細(xì)探討JDK 1.8中ConcurrentHashMap的核心方法,且為什么是線程安全的以及和JDK 1.8之前的又有何區(qū)別。
ConcurrentHashMap底層容器和HashMap相同,同樣是Node數(shù)組+鏈表+紅黑樹(shù),不同的是在原來(lái)的基礎(chǔ)之上使用了Synchronized+CAS來(lái)保證線程安全,下面我們來(lái)進(jìn)行源碼分析。
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());
//這是用來(lái)記錄鏈表的長(zhǎng)度
int binCount = 0;
//table:核心的Node數(shù)組。
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果數(shù)組為空,則進(jìn)行數(shù)組的初始化。這里相當(dāng)于懶漢模式,使用的時(shí)候才去初始化
if (tab == null || (n = tab.length) == 0)
//進(jìn)行數(shù)組的初始化
tab = initTable();
//根據(jù)key的hash計(jì)算出該key在Node數(shù)組中的位置,并判斷這個(gè)位置是否為null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//當(dāng)前的數(shù)組位置為null,則使用CAS插入一個(gè)新的Node
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//表示正在進(jìn)行擴(kuò)容操作 涉及了ForwardingNode 這個(gè)特殊的Node,
//在擴(kuò)容時(shí)會(huì)進(jìn)行創(chuàng)建,且固定傳入的hash值為 -1
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//到了這里表示,出現(xiàn)了hash沖突,key在Node數(shù)組中的索引位置不為null。
//需進(jìn)行鏈表或紅黑樹(shù)的插入操作。
else {
V oldVal = null;
//這個(gè) f 存放是 根據(jù)key在數(shù)組中找到的Node,相當(dāng)于紅黑樹(shù)或鏈表的頭結(jié)點(diǎn),并進(jìn)行加鎖
synchronized (f) {
//這里再進(jìn)行一次判斷,保證f沒(méi)被其它線程修改
if (tabAt(tab, i) == f) {
//如果是鏈表
if (fh >= 0) {
//統(tǒng)計(jì)鏈表的長(zhǎng)度
binCount = 1;
//對(duì)f這個(gè)Node進(jìn)行累加鏈表的長(zhǎng)度,并遍歷鏈表
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;
//遍歷到最后一個(gè)Node,插入一個(gè)新的Node到鏈表的尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果是紅黑樹(shù)
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//使用紅黑樹(shù)的方式進(jìn)行Node的插入
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//判斷鏈表的長(zhǎng)度是否大于TREEIFY_THRESHOLD,是則轉(zhuǎn)紅黑樹(shù)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//統(tǒng)計(jì)整個(gè)的數(shù)組長(zhǎng)度,并判斷是否需要擴(kuò)容
addCount(1L, binCount);
return null;
}好了,以上就是大致的分析內(nèi)容,但還有許多步驟沒(méi)有展開(kāi)代碼詳細(xì)說(shuō)明,如初始化、鏈表轉(zhuǎn)紅黑樹(shù)及擴(kuò)容等,其中擴(kuò)容步驟非常復(fù)雜,有機(jī)會(huì)我會(huì)單獨(dú)寫(xiě)一篇博客詳細(xì)介紹,在這就不多說(shuō)了。我們接下來(lái)介紹較為簡(jiǎn)單的初始化及get方法。
初始化數(shù)組
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//表示已經(jīng)有線程在進(jìn)行初始化操作,讓出CPU的執(zhí)行權(quán)
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//把sc賦值為 -1,表示當(dāng)前線程開(kāi)始執(zhí)行初始化操作
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//獲取數(shù)組的初始長(zhǎng)度,默認(rèn)為16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//初始化數(shù)組
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
//sizeCtl 參數(shù) 第一個(gè)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;
//計(jì)算key的hash
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
//根據(jù)hash值做運(yùn)算獲取數(shù)組對(duì)應(yīng)的Node(相當(dāng)于頭結(jié)點(diǎn))
(e = tabAt(tab, (n - 1) & h)) != null) {
//根據(jù)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;
}
//這里判斷是否正在擴(kuò)容 或者 該節(jié)點(diǎn)為紅黑樹(shù)
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//到了這一步表示該結(jié)構(gòu)為鏈表,遍歷鏈表,返回value
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}好了,核心的源碼部分就分析到這里,我們?cè)趤?lái)看看JDK 1.8之前的ConcurrentHashMap大致是怎么實(shí)現(xiàn)的,區(qū)別相當(dāng)大。
1.8之前的ConcurrentHashMap是采用了ReentrantLock+Segment+HashEntry的結(jié)構(gòu)
整體就是由一個(gè)個(gè) Segment 組成,Segment中包含了HashEntry數(shù)組,HashEntry數(shù)組就是1.8的那個(gè)table,只不過(guò)它這里是多個(gè)。
其中Segment在實(shí)現(xiàn)上繼承了ReentrantLock,這樣就自帶了鎖的功能,它在進(jìn)行put的時(shí)候只會(huì)鎖住一個(gè)Segment,所以理論上,最多可以同時(shí)支持 Segment 個(gè)數(shù)的線程并發(fā)寫(xiě),只要它們的操作分別分布在不同的 Segment 上。get的時(shí)候也是先找到一個(gè)Segment,然后在根據(jù)Hash值找到數(shù)組中的值。
至于為什么JDK在之后使用Synchronized來(lái)保證線程安全,是因?yàn)樵贘DK在版本迭代中一直在對(duì)Synchronized進(jìn)行優(yōu)化,使得Synchronized關(guān)鍵字在某些場(chǎng)景下已經(jīng)不比ReentrantLock效率慢,甚至更快。
到此這篇關(guān)于Java中的ConcurrentHashMap集合源碼解析的文章就介紹到這了,更多相關(guān)ConcurrentHashMap集合源碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jenkins系統(tǒng)如何進(jìn)行數(shù)據(jù)備份
隨著我們的長(zhǎng)期使用,Jenkins系統(tǒng)中的內(nèi)容會(huì)越來(lái)越多,特別是一些配置相關(guān)的東西,不能有任何丟失。這個(gè)時(shí)候我們就需要定期備份我們的Jenkins系統(tǒng),避免一些誤操作不小心刪除了某些重要文件,本文就將介紹下Jenkins系統(tǒng)如何進(jìn)行數(shù)據(jù)備份2021-06-06
FeignMultipartSupportConfig上傳圖片配置方式
這篇文章主要介紹了FeignMultipartSupportConfig上傳圖片配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
把Java程序轉(zhuǎn)換成exe,可直接運(yùn)行的實(shí)現(xiàn)
這篇文章主要介紹了把Java程序轉(zhuǎn)換成exe,可直接運(yùn)行的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
解決SpringMVC Controller 接收頁(yè)面?zhèn)鬟f的中文參數(shù)出現(xiàn)亂碼的問(wèn)題
下面小編就為大家分享一篇解決SpringMVC Controller 接收頁(yè)面?zhèn)鬟f的中文參數(shù)出現(xiàn)亂碼的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
java設(shè)計(jì)簡(jiǎn)單學(xué)生管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java設(shè)計(jì)簡(jiǎn)單學(xué)生管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
servlet簡(jiǎn)單實(shí)現(xiàn)文件下載的方法
這篇文章主要介紹了servlet簡(jiǎn)單實(shí)現(xiàn)文件下載的方法,涉及基于servlet技術(shù)實(shí)現(xiàn)流形式文件傳輸?shù)南嚓P(guān)操作技巧,需要的朋友可以參考下2016-12-12
Java聊天室之實(shí)現(xiàn)聊天室服務(wù)端功能
這篇文章主要為大家詳細(xì)介紹了Java簡(jiǎn)易聊天室之實(shí)現(xiàn)聊天室服務(wù)端功能,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-10-10
myeclipse導(dǎo)出可運(yùn)行jar包簡(jiǎn)介
這篇文章主要介紹了myeclipse導(dǎo)出可運(yùn)行jar包簡(jiǎn)介,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11

