Java源碼解析之ConcurrentHashMap
早期 ConcurrentHashMap,其實現(xiàn)是基于:
- 分離鎖,也就是將內(nèi)部進(jìn)行分段(Segment),里面則是 HashEntry 的數(shù)組,和 HashMap 類似,哈希相同的條目也是以鏈表形式存放。
- HashEntry 內(nèi)部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機(jī)制以改進(jìn)利用 Unsafe 提供的底層能力,比如 volatile access,去直接完成部分操作,以最優(yōu)化性能,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優(yōu)化過的。
在進(jìn)行并發(fā)操作的時候,只需要鎖定相應(yīng)段,這樣就有效避免了類似 Hashtable 整體同步的問題,大大提高了性能。

Put操作
通過二次哈希避免哈希沖突,然后以 Unsafe 調(diào)用方式,直接獲取相應(yīng)的 Segment,然后進(jìn)行線程安全的 put 操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 二次哈希,以保證數(shù)據(jù)的分散性,避免哈希沖突
int hash = hash(key.hashCode());
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
其核心邏輯實現(xiàn)在下面的內(nèi)部方法中:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// scanAndLockForPut 會去查找是否有 key 相同 Node
// 無論如何,確保獲取鎖
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
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;
// 更新已有 value...
}
else {
// 放置 HashEntry 到特定位置,如果超過閾值,進(jìn)行 rehash
// ...
}
}
} finally {
unlock();
}
return oldValue;
}
在寫的時候:
- ConcurrentHashMap 會獲取再入鎖,以保證數(shù)據(jù)一致性,Segment 本身就是基于 ReentrantLock 的擴(kuò)展實現(xiàn),所以,在并發(fā)修改期間,相應(yīng) Segment 是被鎖定的。
- 在最初階段,進(jìn)行重復(fù)性的掃描,以確定相應(yīng) key 值是否已經(jīng)在數(shù)組里面,進(jìn)而決定是更新還是放置操作。
- 在 ConcurrentHashMap 中解決擴(kuò)容的問題,不是整體的擴(kuò)容,而是單獨(dú)對 Segment 進(jìn)行擴(kuò)容。
- 為了減少鎖定segment的開銷,ConcurrentHashMap 的實現(xiàn)是通過重試機(jī)制(RETRIES_BEFORE_LOCK,指定重試次數(shù) 2),來試圖獲得可靠值。如果沒有監(jiān)控到發(fā)生變化(通過對比 Segment.modCount),就直接返回,否則獲取鎖進(jìn)行操作。
機(jī)制在Java 8 上的變化:
- 總體結(jié)構(gòu)上,它的內(nèi)部存儲與HashMap 結(jié)構(gòu)非常相似,同樣是大的桶(bucket)數(shù)組,然后內(nèi)部也是一個個所謂的鏈表結(jié)構(gòu)(bin),同步的粒度要更細(xì)致一些。
- 其內(nèi)部仍然有 Segment 定義,但僅僅是為了保證序列化時的兼容性而已,不再有任何結(jié)構(gòu)上的用處。
- 因為不再使用 Segment,初始化操作大大簡化,修改為 lazy-load 形式,這樣可以有效避免初始開銷。
- 數(shù)據(jù)存儲利用 volatile 來保證可見性。
- 使用 CAS (Compare And Swap)等操作,在特定場景進(jìn)行無鎖并發(fā)操作。
- 使用 Unsafe、LongAdder 之類底層手段,進(jìn)行極端情況的優(yōu)化。
看看在java8上的put操作
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) {
// 利用 CAS 去進(jìn)行無鎖線程安全操作,如果 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 // 不加鎖,進(jìn)行檢查
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 細(xì)粒度的同步修改操作...
}
}
// Bin 超過閾值,進(jìn)行樹化
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
初始化操作實現(xiàn)在 initTable 里面,這是一個典型的 CAS 使用場景,利用 volatile 的 sizeCtl 作為互斥手段:如果發(fā)現(xiàn)競爭性的初始化,就 spin 在那里,等待條件恢復(fù);否則利用 CAS 設(shè)置排他標(biāo)志。如果成功則進(jìn)行初始化;否則重試。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 如果發(fā)現(xiàn)沖突,進(jìn)行 spin 等待
if ((sc = sizeCtl) < 0)
Thread.yield();
// CAS 成功返回 true,則進(jìn)入真正的初始化邏輯
else if (U.compareAndSetInt(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;
}
當(dāng) bin 為空時,同樣是沒有必要鎖定,也是以 CAS 操作去放置。
到此這篇關(guān)于Java源碼解析之ConcurrentHashMap的文章就介紹到這了,更多相關(guān)Java ConcurrentHashMap內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
十五道tomcat面試題,為數(shù)不多的機(jī)會!
這篇文章主要介紹了十五道tomcat面試題,Tomcat的本質(zhì)是一個Servlet容器。一個Servlet能做的事情是:處理請求資源,并為客戶端填充response對象,需要的朋友可以參考下2021-08-08
SpringBoot項目中使用Sharding-JDBC實現(xiàn)讀寫分離的詳細(xì)步驟
Sharding-JDBC是一個分布式數(shù)據(jù)庫中間件,它不僅支持?jǐn)?shù)據(jù)分片,還可以輕松實現(xiàn)數(shù)據(jù)庫的讀寫分離,本文介紹如何在Spring Boot項目中集成Sharding-JDBC并實現(xiàn)讀寫分離的詳細(xì)步驟,需要的朋友可以參考下2024-08-08
使用Prometheus+Grafana的方法監(jiān)控Springboot應(yīng)用教程詳解
這篇文章主要介紹了用Prometheus+Grafana的方法監(jiān)控Springboot應(yīng)用,本文通過實例代碼詳解給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
IDEA中程序包Org.Springframework.Boot不存在問題及解決
這篇文章主要介紹了IDEA中程序包Org.Springframework.Boot不存在問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
BeanDefinitionRegistryPostProcessor如何動態(tài)注冊Bean到Spring
這篇文章主要介紹了BeanDefinitionRegistryPostProcessor如何動態(tài)注冊Bean到Spring,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03

