ConcurrentHashMap之實現細節
是Java 5中支援高併發、高吞吐量的執行緒安全HashMap實現。在這之前我對ConcurrentHashMap只有一些膚淺的理解,僅知道它採用了多個鎖,大概也足夠了。但是在經過一次慘痛的面試經歷之後,我覺得必須深入研究它的實現。面試中被問到讀是否要加鎖,因為讀寫會發生衝突,我說必須要加鎖,我和麵試官也因此發生了衝突,結果可想而知。還是閒話少說,通過仔細閱讀原始碼,現在總算理解ConcurrentHashMap實現機制了,其實現之精巧,令人歎服,與大家共享之。
實現原理
鎖分離 (Lock Stripping)
ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的hash table,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。
有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。這裡“按順序”是很重要的,否則極有可能出現死鎖,在ConcurrentHashMap內部,段陣列是final的,並且其成員變數實際上也是final的,但是,僅僅是將陣列宣告為final的並不保證陣列成員也是final的,這需要實現上的保證。這可以確保不會出現死鎖,因為獲得鎖的順序是固定的。不變性是多執行緒程式設計佔有很重要的地位,下面還要談到。
final Segment<K,V>[] segments;
不變(Immutable)和易變(Volatile)
ConcurrentHashMap完全允許多個讀操作併發進行,讀操作並不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間新增或刪除元素,讀操作不加鎖將得到不一致的資料。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,其結構如下所示:
static final class HashEntry<K,V> {final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
可以看到除了value不是final的,其它值都是final的,這意味著不能從hash鏈的中間或尾部新增或刪除節點,因為這需要修改next引用值,所有的節點的修改只能從頭部開始。對於put操作,可以一律新增到Hash鏈的頭部。但是對於remove操作,可能需要從中間刪除一個節點,這就需要將要刪除節點的前面所有節點整個複製一遍,最後一個節點指向要刪除結點的下一個結點。這在講解刪除操作時還會詳述。為了確保讀操作能夠看到最新的值,將value設定成volatile,這避免了加鎖。
其它
為了加快定位段以及段中hash槽的速度,每個段hash槽的的個數都是2^n,這使得通過位運算就可以定位段和段中hash槽的位置。當併發級別為預設值16時,也就是段的個數,hash值的高4位決定分配在哪個段中。但是我們也不要忘記《演算法導論》給我們的教訓:hash槽的的個數不應該是2^n,這可能導致hash槽分配不均,這需要對hash值重新再hash一次。(這段似乎有點多餘了 )
這是重新hash的演算法,還比較複雜
private static int hash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
這是定位段的方法:
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
資料結構
關於Hash表的基礎資料結構,這裡不想做過多的探討。Hash表的一個很重要方面就是如何解決hash衝突,ConcurrentHashMap和HashMap使用相同的方式,都是將hash值相同的節點放在一個hash鏈中。與HashMap不同的是,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的資料成員:
ConcurrentHashMap
類中包含兩個靜態內部類 HashEntry 和 Segment。HashEntry 用來封裝對映表的鍵 / 值對;Segment 用來充當鎖的角色,每個 Segment 物件守護整個雜湊對映表的若干個桶。每個桶是由若干個 HashEntry 物件連結起來的連結串列。一個 ConcurrentHashMap 例項中包含由若干個 Segment 物件組成的陣列。
static
final class HashEntry<K,V> {
final K key; // 宣告 key 為 final 型
final int hash; // 宣告 hash 值為 final 型
volatile V value; // 宣告 value 為 volatile 型
final HashEntry<K,V> next; // 宣告 next 為 final 型
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
static
final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count; //在本 segment 範圍內,包含的 HashEntry 元素的個數
//volatile 型
transient int modCount; //table 被更新的次數
transient int threshold; //預設容量
final float loadFactor; //裝載因子
/**
* table 是由 HashEntry 物件組成的陣列
* 如果雜湊時發生碰撞,碰撞的 HashEntry 物件就以連結串列的形式連結成一個連結串列
* table 陣列的陣列成員代表雜湊對映表的一個桶
*/
transient volatile HashEntry<K,V>[] table;
/**
* 根據 key 的雜湊值,找到 table 中對應的那個桶(table 陣列的某個陣列成員)
* 把雜湊值與 table 陣列長度減 1 的值相“與”,得到雜湊值對應的 table 陣列的下標
* 然後返回 table 陣列中此下標對應的 HashEntry 元素
* 即這個段中連結串列的第一個元素
*/
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
Segment(int initialCapacity, float lf) {
loadFactor = lf;
setTable(HashEntry.<K,V>newArray(initialCapacity));
}
/**
* 設定 table 引用到這個新生成的 HashEntry 陣列
* 只能在持有鎖或建構函式中呼叫本方法
*/
void setTable(HashEntry<K,V>[] newTable) {
threshold = (int)(newTable.length * loadFactor);
table = newTable;
}
}
get操作不需要鎖。第一步是訪問count變數,這是一個volatile變數,由於所有的修改操作在進行結構修改時都會在最後一步寫count變數,通過這種機制保證get操作能夠得到幾乎最新的結構更新。對於非結構更新,也就是結點值的改變,由於HashEntry的value變數是volatile的,也能保證讀取到最新的值。接下來就是對hash鏈進行遍歷找到要獲取的結點,如果沒有找到,直接訪回null。對hash鏈進行遍歷不需要加鎖的原因在於鏈指標next是final的。但是頭指標卻不是final的,這是通過getFirst(hash)方法返回,也就是存在table陣列中的值。這使得getFirst(hash)可能返回過時的頭結點,例如,當執行get方法時,剛執行完getFirst(hash)之後,另一個執行緒執行了刪除操作並更新頭結點,這就導致get方法中返回的頭結點不是最新的。這是可以允許,通過對count變數的協調機制,get能讀取到幾乎最新的資料,雖然可能不是最新的。要得到最新的資料,只有採用完全的同步。
探索 ConcurrentHashMap 高併發性的實現機制:
http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/
ConcurrentHashMap之實現細節
http://www.iteye.com/topic/344876
Map的併發處理(ConcurrentHashMap)
http://zl198751.iteye.com/blog/907927
集合框架 Map篇(4)----ConcurrentHashMap
http://hi.baidu.com/yao1111yao/blog/item/232f2dfc55fbcd5ad7887d9f.html
java ConcurrentHashMap中的一點點迷惑
http://icanfly.iteye.com/blog/1450165
相關文章
- 原始碼閱讀之LinkedList實現細節原始碼
- 原始碼閱讀之ArrayList實現細節原始碼
- 使用Covermap實現地形細節
- 深入探究JVM之垃圾回收演算法實現細節JVM演算法
- 多執行緒十二之ConcurrentHashMap1.8實現分析執行緒HashMap
- 使用ConcurrentHashMap實現快取HashMap快取
- 理解virtual dom的實現細節-snabbdom
- 迴圈佇列的實現及細節佇列
- ConcurrentHashMap 實現原理和原始碼分析HashMap原始碼
- 利用ConcurrentHashMap來實現一個ConcurrentHashSetHashMap
- Promise 規範解讀及實現細節 (二)Promise
- COP4600 檔案系統實現細節
- 【freertos】006-任務切換實現細節
- Redis高可用之哨兵機制實現細節Redis
- Dubbo2.7的Dubbo SPI實現原理細節
- HDFS 原始碼解讀:HadoopRPC 實現細節的探究原始碼HadoopRPC
- Spartacus 註冊和登入頁面的實現細節
- 【freertos】012-事件標誌概念和實現細節事件
- 【freertos】007-系統節拍和系統延時管理實現細節
- 探索 YOLO v3 實現細節 - 第5篇 LossYOLO
- 探索 YOLO v3 實現細節 - 第2篇 模型YOLO模型
- Dapr實現分散式有狀態服務的細節分散式
- 探索 YOLO v3 實現細節 – 第2篇 模型YOLO模型
- 細節解析 JavaScript 中 bind 函式的模擬實現JavaScript函式
- ConcurrentHashMap 併發之美HashMap
- 探索 YOLO v3 實現細節 - 第3篇 網路YOLO
- Rhinoceros 8:實現細節完美的三維建模 mac/win版ROSMac
- C++17 std::variant 詳解:概念、用法和實現細節C++
- 【freertos】010-訊息佇列概念及其實現細節佇列
- 【freertos】004-任務建立與刪除及其實現細節
- Java ConcurrentHashMap 高併發安全實現原理解析JavaHashMap
- JVM(四)垃圾回收的實現演算法和執行細節JVM演算法
- LayIM.AspNetCore Middleware 開發日記(五)Init介面實現細節NetCore
- [譯] ES6:理解引數預設值的實現細節
- SAP 電商雲 Spartacus UI 的響應式 UI 實現細節UI
- FSM自動售貨機 verilog 實現及 code 細節講解
- 小白細節思考之讀取Request.Body
- 小細節
- Laravel核心解讀–使用者認證系統的實現細節Laravel