ConcurrentHashMap的鎖分離技術(原始碼)
ConcurrentHashMap的鎖分離技術
concurrenthashmap是一個非常好的map實現,在高併發操作的場景下會有非常好的效率。實現的目的主要是為了避免同步操作時對整個map物件進行鎖定從而提高併發訪問能力。
ConcurrentHashMap 類中包含兩個靜態內部類 HashEntry 和 Segment。HashEntry 用來封裝對映表的鍵 / 值對;Segment 用來充當鎖的角色,每個 Segment 物件守護整個雜湊對映表的若干個桶。每個桶是由若干個 HashEntry 物件連結起來的連結串列。一個 ConcurrentHashMap 例項中包含由若干個 Segment 物件組成的陣列。
Java程式碼
- 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;
- }
- }
Java程式碼
- 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;
- }
- }
注意Segment繼承了ReentrantLock 鎖
左邊便是Hashtable的實現方式---鎖整個hash表;而右邊則是ConcurrentHashMap的實現方式---鎖桶(或段)。 ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。試想,原來 只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,而讀執行緒幾乎不受限制,之後會提到),併發性的提升是顯而易見的。
更令人驚訝的是ConcurrentHashMap的讀取併發,因為在讀取的大多數時候都沒有用到鎖定,所以讀取操作幾乎是完全的併發操作,而寫操作鎖定的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現得更明顯些)。只有在求size等操作時才需要鎖定整個表。而在迭代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱為弱一致迭代器。在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的資料,iterator完成後再將頭指標替換為新的資料,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變,更重要的,這保證了多個執行緒併發執行的連續性和擴充套件性,是效能提升的關鍵。
接下來,讓我們看看ConcurrentHashMap中的幾個重要方法,心裡知道了實現機制後,使用起來就更加有底氣。
ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關係。
get方法(請注意,這裡分析的方法都是針對桶的,因為ConcurrentHashMap的最大改進就是將粒度細化到了桶上),首先判斷了當前桶的資料 個數是否為0,為0自然不可能get到什麼,只有返回null,這樣做避免了不必要的搜尋,也用最小的代價避免出錯。然後得到頭節點(方法將在下面涉及) 之後就是根據hash和key逐個判斷是否是指定的值,如果是並且值非空就說明找到了,直接返回;程式非常簡單,但有一個令人困惑的地方,這句 return readValueUnderLock(e)到底是用來幹什麼的呢?研究它的程式碼,在鎖定之後返回一個值。但這裡已經有一句V v = e.value得到了節點的值,這句return readValueUnderLock(e)是否多此一舉?事實上,這裡完全是為了併發考慮的,這裡當v為空時,可能是一個執行緒正在改變節點,而之前的get操作都未進行鎖定,根據bernstein條件,讀後寫或寫後讀都會引起資料的不一致,所以這裡要對這個e重新上鎖再讀一遍,以保證得到的是正確值,這裡不得不佩服Doug Lee思維的嚴密性。整個get操作只有很少的情況會鎖定,相對於之前的Hashtable,併發是不可避免的啊!
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能讀取到幾乎最新的資料,雖然可能不是最新的。要得到最新的資料,只有採用完全的同步。
- V get(Object key, int hash) {
- if (count != 0) { // read-volatile
- HashEntry 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;
- }
- V readValueUnderLock(HashEntry e) {
- lock();
- try {
- return e.value;
- } finally {
- unlock();
- }
- }
put操作一上來就鎖定了整個segment,這當然是為了併發的安全,修改資料是不能併發進行的,必須得有個判斷是否超限的語句以確保容量不足時能夠rehash,而比較難懂的是這句int index = hash & (tab.length - 1),原來segment裡面才是真正的hashtable,即每個segment是一個傳統意義上的hashtable,如上圖,從兩者的結構就可以看出區別,這裡就是找出需要的entry在table的哪一個位置,之後得到的entry就是這個鏈的第一個節點,如果e!=null,說明找到了,這是就要替換節點的值(onlyIfAbsent == false),否則,我們需要new一個entry,它的後繼是first,而讓tab[index]指向它,什麼意思呢?實際上就是將這個新entry插入到鏈頭,剩下的就非常容易理解了。
- V put(K key, int hash, V value, boolean onlyIfAbsent) {
- lock();
- try {
- int c = count;
- if (c++ > threshold) // ensure capacity
- rehash();
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry) tab[index];
- HashEntry 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(key, hash, first, value);
- count = c; // write-volatile
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
remove操作非常類似put,但要注意一點區別,中間那個for迴圈是做什麼用的呢?(*號標記)從程式碼來看,就是將定位之後的所有entry克隆並拼回前面去,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍?這點其實是由entry 的不變性來決定的,仔細觀察entry定義,發現除了value,其他所有屬性都是用final來修飾的,這意味著在第一次設定了next域之後便不能再 改變它,取而代之的是將它之前的節點全都克隆一次。至於entry為什麼要設定為不變性,這跟不變性的訪問不需要同步從而節省時間有關,關於不變性的更多 內容,請參閱之前的文章《執行緒高階---執行緒的一些程式設計技巧》
- V remove(Object key, int hash, Object value) {
- lock();
- try {
- int c = count - 1;
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry)tab[index];
- HashEntry 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 newFirst = e.next;
- * for (HashEntry p = first; p != e; p = p.next)
- * newFirst = new HashEntry(p.key, p.hash,
- newFirst, p.value);
- tab[index] = newFirst;
- count = c; // write-volatile
- }
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
探索 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中的一點點迷惑