Java?ConcurrentHashMap實(shí)現(xiàn)線程安全的代碼示例
ConcurrentHashMap 線程安全實(shí)現(xiàn)簡(jiǎn)述
ConcurrentHashMap 在 JDK 1.7 時(shí),使用的是分段鎖也就是 Segment 來(lái)實(shí)現(xiàn)線程安全的。
然而它在 JDK 1.8 之后,使用的是 CAS + synchronized 或 CAS + volatile 來(lái)實(shí)現(xiàn)線程安全的。
JDK 1.7 底層結(jié)構(gòu)
ConcurrentHashMap 在不同的 JDK 版本中實(shí)現(xiàn)是不同的,在 JDK 1.7 中它使用的是數(shù)組加鏈表的形式實(shí)現(xiàn)的,而數(shù)組又分為:大數(shù)組 Segment 和小數(shù)組 HashEntry。 大數(shù)組 Segment 可以理解為 MySQL 中的數(shù)據(jù)庫(kù),而每個(gè)數(shù)據(jù)庫(kù)(Segment)中又有很多張表 HashEntry,每個(gè) HashEntry 中又有多條數(shù)據(jù),這些數(shù)據(jù)是用鏈表連接的,如下圖所示:

JDK 1.7 線程安全實(shí)現(xiàn)
了解了 ConcurrentHashMap 的底層實(shí)現(xiàn),再看它的線程安全實(shí)現(xiàn)就比較簡(jiǎn)單了。
接下來(lái),我們通過(guò)添加元素 put 方法,來(lái)看 JDK 1.7 中 ConcurrentHashMap 是如何保證線程安全的,具體實(shí)現(xiàn)源碼如下:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往該 Segment 寫(xiě)入前,先確保獲取到鎖
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
// Segment 內(nèi)部數(shù)組
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有值...
}
else {
// 放置 HashEntry 到特定位置,如果超過(guò)閾值則進(jìn)行 rehash
// 忽略其他代碼...
}
}
} finally {
// 釋放鎖
unlock();
}
return oldValue;
}從上述源碼我們可以看出,Segment 本身是基于 ReentrantLock 實(shí)現(xiàn)的加鎖和釋放鎖的操作,這樣就能保證多個(gè)線程同時(shí)訪問(wèn) ConcurrentHashMap 時(shí),同一時(shí)間只有一個(gè)線程能操作相應(yīng)的節(jié)點(diǎn),這樣就保證了 ConcurrentHashMap 的線程安全了。
也就是說(shuō) ConcurrentHashMap 的線程安全是建立在 Segment 加鎖的基礎(chǔ)上的,所以我們把它稱之為分段鎖或片段鎖,如下圖所示:

JDK 1.8 底層結(jié)構(gòu)
在 JDK 1.7 中,ConcurrentHashMap 雖然是線程安全的,但因?yàn)樗牡讓訉?shí)現(xiàn)是數(shù)組 + 鏈表的形式,所以在數(shù)據(jù)比較多的情況下訪問(wèn)是很慢的,因?yàn)橐闅v整個(gè)鏈表,而 JDK 1.8 則使用了數(shù)組 + 鏈表/紅黑樹(shù)的方式優(yōu)化了 ConcurrentHashMap 的實(shí)現(xiàn),具體實(shí)現(xiàn)結(jié)構(gòu)如下:

鏈表升級(jí)為紅黑樹(shù)的規(guī)則:當(dāng)鏈表長(zhǎng)度大于 8,并且數(shù)組的長(zhǎng)度大于 64 時(shí),鏈表就會(huì)升級(jí)為紅黑樹(shù)的結(jié)構(gòu)。
PS:ConcurrentHashMap 在 JDK 1.8 雖然保留了 Segment 的定義,但這僅僅是為了保證序列化時(shí)的兼容性,不再有任何結(jié)構(gòu)上的用處了。
JDK 1.8 線程安全實(shí)現(xiàn)
在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式來(lái)保證線程安全的,它的核心實(shí)現(xiàn)源碼如下:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 節(jié)點(diǎn)為空
// 利用 CAS 去進(jìn)行無(wú)鎖線程安全操作,如果 bin 是空的
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 細(xì)粒度的同步修改操作...
}
}
// 如果超過(guò)閾值,升級(jí)為紅黑樹(shù)
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}從上述源碼可以看出,在 JDK 1.8 中,添加元素時(shí)首先會(huì)判斷容器是否為空,如果為空則使用 volatile 加 CAS 來(lái)初始化。如果容器不為空則根據(jù)存儲(chǔ)的元素計(jì)算該位置是否為空,如果為空則利用 CAS 設(shè)置該節(jié)點(diǎn);如果不為空則使用 synchronize 加鎖,遍歷桶中的數(shù)據(jù),替換或新增節(jié)點(diǎn)到桶中,最后再判斷是否需要轉(zhuǎn)為紅黑樹(shù),這樣就能保證并發(fā)訪問(wèn)時(shí)的線程安全了。
我們把上述流程簡(jiǎn)化一下,我們可以簡(jiǎn)單的認(rèn)為在 JDK 1.8 中,ConcurrentHashMap 是在頭節(jié)點(diǎn)加鎖來(lái)保證線程安全的,鎖的粒度相比 Segment 來(lái)說(shuō)更小了,發(fā)生沖突和加鎖的頻率降低了,并發(fā)操作的性能就提高了。而且 JDK 1.8 使用的是紅黑樹(shù)優(yōu)化了之前的固定鏈表,那么當(dāng)數(shù)據(jù)量比較大的時(shí)候,查詢性能也得到了很大的提升,從之前的 O(n) 優(yōu)化到了 O(logn) 的時(shí)間復(fù)雜度,具體加鎖示意圖如下:

小結(jié)
ConcurrentHashMap 在 JDK 1.7 時(shí)使用的是數(shù)據(jù)加鏈表的形式實(shí)現(xiàn)的,其中數(shù)組分為兩類(lèi):大數(shù)組 Segment 和小數(shù)組 HashEntry,而加鎖是通過(guò)給 Segment 添加 ReentrantLock 鎖來(lái)實(shí)現(xiàn)線程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是數(shù)組+鏈表/紅黑樹(shù)的方式實(shí)現(xiàn)的,它是通過(guò) CAS 或 synchronized 來(lái)實(shí)現(xiàn)線程安全的,并且它的鎖粒度更小,查詢性能也更高。
以上就是Java ConcurrentHashMap實(shí)現(xiàn)線程安全的代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Java ConcurrentHashMap線程安全的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringCloud兩種注冊(cè)中心的實(shí)現(xiàn)
本文主要介紹了SpringCloud兩種注冊(cè)中心的實(shí)現(xiàn),包括服務(wù)注冊(cè)、服務(wù)拉取、負(fù)載均衡和實(shí)例權(quán)重配置等,具有一定的參考價(jià)值,感興趣的可以了解一下2025-01-01
Java中CompletableFuture四種調(diào)用模式的實(shí)現(xiàn)
CompletableFuture有就地執(zhí)行、異步執(zhí)行等三種模式,就地執(zhí)行適合輕量級(jí)任務(wù),但可能導(dǎo)致線程阻塞,異步執(zhí)行有上下文切換開(kāi)銷(xiāo),本文介紹了第四種調(diào)用模式,解決了以上問(wèn)題2025-08-08
MySQL text類(lèi)型對(duì)應(yīng)mybatis jdbcType類(lèi)型方式
這篇文章主要介紹了MySQL text類(lèi)型對(duì)應(yīng)mybatis jdbcType類(lèi)型方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
淺析JavaMail發(fā)送郵件后再通過(guò)JavaMail接收格式問(wèn)題
這篇文章主要介紹了JavaMail發(fā)送郵件后再通過(guò)JavaMail接收格式問(wèn)題 ,本文通過(guò)代碼實(shí)例給大家詳細(xì)解說(shuō),需要的朋友可以參考下2019-06-06
Java?Jar包項(xiàng)目?jī)?nèi)存設(shè)置方法舉例
這篇文章主要給大家介紹了關(guān)于Java?Jar包項(xiàng)目?jī)?nèi)存設(shè)置方法的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01
Java學(xué)生信息管理系統(tǒng)設(shè)計(jì)(數(shù)據(jù)庫(kù)版)
這篇文章主要為大家詳細(xì)介紹了數(shù)據(jù)庫(kù)版的Java學(xué)生信息管理系統(tǒng)設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Spring事務(wù)Transaction配置的五種注入方式詳解
這篇文章主要介紹了Spring事務(wù)Transaction配置的五種注入方式詳解,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04
SpringBoot?Java通過(guò)API的方式調(diào)用騰訊智能體(騰訊元寶)代碼示例
這篇文章主要介紹了SpringBoot?Java通過(guò)API的方式調(diào)用騰訊智能體(騰訊元寶)的相關(guān)資料,詳細(xì)說(shuō)明參數(shù)獲取及動(dòng)態(tài)處理方法,提供結(jié)果字符串轉(zhuǎn)數(shù)組技巧,需要的朋友可以參考下2025-06-06

