ConcurrentHashMap 實現原理和原始碼分析
結構
HashMap結構圖 static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
static final class Segment<K,V> extends ReentrantLock implements Serializableq
每個segment使用物件自身的鎖來實現。只有對全域性需要改變時鎖定的是所有的segment。
這種做法,就稱之為“分離鎖(lock striping)
分拆鎖(lock spliting)就是若原先的程式中多處邏輯都採用同一個鎖,但各個邏輯之間又相互獨立,就可以拆(Spliting)為使用多個鎖,每個鎖守護不同的邏輯。
分拆鎖有時候可以被擴充套件,分成可大可小加鎖塊的集合,並且它們歸屬於相互獨立的物件,這樣的情況就是分離鎖(lock striping)。(摘自《Java併發程式設計實踐》)
初始化
它把區間按照併發級別(concurrentLevel),分成了若干個segment。預設情況下內部按併發級別為16來建立。對於每個segment的容量,預設情況也是16。concurrentLevel,segment 可以通過建構函式設定的。通過按位與的雜湊演算法來定位segments陣列的索引,必須保證segments陣列的長度是2的N次方(power-of-two size),所以必須計算出一個是大於或等於concurrencyLevel的最小的2的N次方值來作為segments陣列的長度。
定位Segment
ConcurrentHashMap會首先使用Wang/Jenkins hash的變種演算法對元素的hashCode進行一次再雜湊,其目的是為了減少雜湊衝突,使元素能夠均勻的分佈在不同的Segment上,從而提高容器的存取效率。預設情況下segmentShift為28,segmentMask為15,再雜湊後的數最大是32位二進位制資料,向右無符號移動28位,意思是讓高4位參與到hash運算中, (hash >>> segmentShift) & segmentMask的運算結果分別是4,15,7和8,可以看到hash值沒有發生衝突
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
remove操作
不同segment,可以併發操作
public V remove(Object key) {
hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null);
}
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
for (HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
如果節點不存在就直接返回null,否則就要將e前面的結點複製一遍,尾結點指向e的下一個結點。因為entry除了value是volatile屬性,其他都是final修飾的,所以不能重新對next域賦值,只能重新clone一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關get操作
Segment的get方法
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
V get(Object key, int hash) {
if (count != 0) { // read-volatile 當前桶的資料個數是否為0
HashEntry<K,V> e = getFirst(hash); 得到頭節點
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空的才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,原因是它的get方法裡將要使用的共享變數都定義成volatile,在get操作裡只需要讀不需要寫共享變數count和value,所以可以不用加鎖。之所以不會讀到過期的值,是根據java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個執行緒同時修改和獲取volatile變數,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景。
理論上結點的值不可能為空,這是因為 put的時候,如果為空就要拋NullPointerException。空值是因為put,remove操作時候,tab[index] = new HashEntry<K,V>(key, hash, first, value)重新克隆了節點前的資料。
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
put操作
Segment的put方法。
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
第一步判斷是否需要對Segment裡的HashEntry陣列進行擴容,第二步定位新增元素的位置然後放在HashEntry陣列裡。在插入元素前會先判斷Segment裡的HashEntry陣列是否超過容量(threshold),如果超過閥值,陣列進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容。
擴容的時候首先會建立一個兩倍於原容量的陣列,然後將原陣列裡的元素進行再hash後插入到新的陣列裡。為了高效ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。
containsKey操作
boolean containsKey(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key))
returntrue;
e = e.next;
}
}
returnfalse;
}
size()操作
如果我們要統計整個ConcurrentHashMap裡元素的大小,就必須統計所有Segment裡元素的大小後求和。Segment裡的全域性變數count是一個volatile變數,那麼在多執行緒場景下,我們是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?不是的,雖然相加時可以獲取每個Segment的count的最新值,但是拿到之後可能累加前使用的count發生了變化,那麼統計結果就不準了。所以最安全的做法,是在統計size的時候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效。因為在累加count操作過程中,之前累加過的count發生變化的機率非常小,所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。
那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變數,在put , remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。
參考資料
- 《Java併發程式設計實踐》
- 深入分析ConcurrentHashMap
相關文章
- ConcurrentHashMap 原始碼分析HashMap原始碼
- hashmap和concurrenthashmap原始碼分析(1.7/1.8)HashMap原始碼
- HashMap實現原理及原始碼分析HashMap原始碼
- HashMap 實現原理與原始碼分析HashMap原始碼
- OpenMP For Construct dynamic 排程方式實現原理和原始碼分析Struct原始碼
- OPENMP FOR CONSTRUCT GUIDED 排程方式實現原理和原始碼分析StructGUIIDE原始碼
- ConcurrentHashMap原始碼分析-JDK18HashMap原始碼JDK
- OpenMP task construct 實現原理以及原始碼分析Struct原始碼
- OpenMP Parallel Construct 實現原理與原始碼分析ParallelStruct原始碼
- OpenMP Sections Construct 實現原理以及原始碼分析Struct原始碼
- Spring原始碼分析之 lazy-init 實現原理Spring原始碼
- 從原始碼分析ConcurrentHashMap執行緒安全和高效的特性原始碼HashMap執行緒
- musl中strlen原始碼實現和分析原始碼
- 原始碼分析–ConcurrentHashMap與HashTable(JDK1.8)原始碼HashMapJDK
- 【原始碼&庫】Vue3 的響應式核心 reactive 和 effect 實現原理以及原始碼分析原始碼VueReact
- JDK動態代理實現原理詳解(原始碼分析)JDK原始碼
- ConcurrentHashMap原始碼解析HashMap原始碼
- 死磕 java集合之ConcurrentHashMap原始碼分析(一)JavaHashMap原始碼
- ConcurrentHashMap 原始碼分析03之內部類ReduceTaskHashMap原始碼
- 原始碼|ThreadLocal的實現原理原始碼thread
- Promise實現原理(附原始碼)Promise原始碼
- redis個人原始碼分析2---dict的實現原理Redis原始碼
- Java面試題 從原始碼角度分析HashSet實現原理?Java面試題原始碼
- Spring Ioc原始碼分析系列--@Autowired註解的實現原理Spring原始碼
- 原始碼分析 Alibaba sentinel 滑動視窗實現原理(文末附原理圖)原始碼
- HashMap原始碼實現分析HashMap原始碼
- Kubernetes Job Controller 原理和原始碼分析(一)Controller原始碼
- ConcurrentHashMap執行緒安全機制以及原始碼分析HashMap執行緒原始碼
- 【原始碼分析】Lottie 實現炫酷動畫背後的原理原始碼動畫
- OpenMP 執行緒同步 Construct 實現原理以及原始碼分析(上)執行緒Struct原始碼
- OpenMP 執行緒同步 Construct 實現原理以及原始碼分析(下)執行緒Struct原始碼
- ConcurrentHashMap原始碼解讀HashMap原始碼
- ConcurrentHashMap原始碼閱讀HashMap原始碼
- 深入原始碼解析 tapable 實現原理原始碼
- Netty原始碼解析 -- PoolChunk實現原理Netty原始碼
- synchronized實現原理及ReentrantLock原始碼synchronizedReentrantLock原始碼
- Netty原始碼解析 -- PoolSubpage實現原理Netty原始碼
- SpringMVC原始碼分析原理SpringMVC原始碼