ConcurrentHashMap 可以做到無鎖讀,而寫使用分段鎖機制,把整個哈希表切分成段segment(默認為16段),每段有一個鎖,最多可以同時有16個寫線程。而讀不受限制。
下文轉自http://taozeyu.com
ConcurrentHashMap是一個線程安全的哈希實現類,它不但能使多線程同時操作該類時保證線程是安全的,同時為了保證對Map的讀操作的高效,完全不使用同步鎖。
實現單線程,或簡單通過加鎖來實現線程安全的一個哈希表所用到的數據結構知識是很普通的,但如果不加鎖,也能保證線程安全,則需要用到一些“奇技淫巧”了。
編寫JDK的程序員正是這么一些掌握了這種“奇技淫巧”的人。,而ConcurrentHashMap類正是運用了這些技巧的一個實現類。本文將用圖片的形式,展現這些技巧。畢竟源代碼的閱讀是枯燥的。
Java如何實現HashMap
Java規定只有對象才能作為哈希表的Key或Value,這意味着基本數據類型只能存它們的封裝對象。一切Java對象都繼承自Object,而Object有兩個與HashMap緊密相關的方法:
int hashCode()
boolean equals(Object o)
哈希表通過Key對象的hashCode()獲取該Key的哈希索引,再通過哈希函數將索引映射到哈希表的某個偏移地址。哈希索引和它經過哈希函數處理后的結果是一個多對一的關系。即同一個哈希索引只可能映射到唯一一個偏移地址上,但同一個偏移地址可能映射到多個哈希索引。
如果多個Key對象被映射到同一個偏移地址(哈希表的設計應該盡量避免這種情況,但有必須對這種情況進行必要的處理) ,稱之為沖突。Java解決沖突的方法是拉鏈法。即多個節點通過鏈表的形式,占用同一個偏移地址。最終結果可能是這個樣子。
當哈希表需要將一個輸入的Key對象映射到特定的節點上時,會以如下方式進行映射:
- 通過Key對象的hashCode()方法獲取哈希索引。
- 用哈希函數算出索引對應的偏移地址。ava實現的哈希函數很簡單,令哈希索引除以數組長度,取余數即為偏移地址。
但Java不是簡單的取余,它令數組的長度永遠是2的整次冪,並以下列式子計算偏移地址。這種算法等價於取余,但效率更高。
hashCode & (hashTable.length-1) - 在數組中查看偏移地址信息,如果為空,則沒有找到。否則則找到一條鏈表。
- 遍歷這條鏈表,對於鏈表每一個節點記錄的Key對象調用equals方法嘗試是否與輸入的Key對象相等。
- 如果找到,則停止遍歷。如果遍歷完鏈表尚未找到,則宣布沒有找到。
由此可見,一切存入HashMap中的Key對象,如果想要重寫hashCode與equals方法中的任意一個,則必須兩個都配套重寫。任何企圖只重寫其中一個方法,或使兩個方法不匹配的,都會令程序產生不可預料的結果。
ConcurrentHashMap的實現
但凡單線程的哈希表實現都是很簡單的,其知識無外乎《數據結構》中那些要領。但要實現無鎖的線程安全的哈希表,則需要一些“巧力”了。讓我們看看JDK的程序員是如何做的吧。
ConcurrentHashMap 對於所有的讀操作,都不加鎖。它僅僅對寫操作加鎖。這意味着僅僅寫操作是互斥的,而讀操作則完全不可預測。
首先讓我們來看看HashMap中的節點,Entry對象。嚴格的Entry泛型定義應該是Entry<key,value>,這樣就限制了Key和Value的類型。
在 ConcurrentHashMap中,寫入的Entry通過無比巧妙的方式,保證了隨時可能進行的讀操作的安全。我將一一介紹它們的具體實現,並配上圖片。
我在配圖的時候將用顏色區分不同的節點:
綠色:一切線程都可以看到的對象,因此這些對象必須假定它隨時都被訪問。
紅色:寫入線程獨占的對象,只有寫入線程可見,對於其他任何線程都是透明的。(由於寫入線程持有了鎖,因此實際上只有一個線程可以訪問到這些對象。)
藍色:即將回收的對象。這些對象可能可見,但是很快隨着方法的返回,這些對象將最終變得不可達,而被垃圾回收期搜集處理掉。
(1)put方法
put方法如果發生在Key已存在的情況下,則僅僅是定位Entry,並將它的Value替換成新的。這種情況下完全不需要加鎖,且能保證線程安全。因此,我不打算討論這種情況。我要討論的是當put發生在Key不存在的情況下的實現。
這種情況下,寫入線程首先將找到偏移地址,並遍歷整個鏈表,但發現鏈表中沒有一個Key是可以匹配的,因此線程必須建立一個新的節點。
這種技巧的關鍵在於,在最后一步完成前,寫入線程做造成的影響全部都是“紅色”的,即外界不可見的。如果你忽視掉“紅色”的方塊,則會發現綠色的方塊在整個過程中都沒有發生任何改變。正是這個特征保證了線程的安全。
(2)remove方法
寫入線程首先,找到需要刪除的節點。再將需要刪除的節點所在鏈表的前趨全部復制一遍,但直接前趨的Next是指向需要刪除節點的后繼的。最后,將復制的前趨替換掉之前的前趨。
這種做法等於將待刪節點,以及它的前趨的全部前趨都刪除掉了(因為變得不可達了),但卻為其前趨做了副本,因此真正被刪除掉的只有待刪節點本身而已。
注意第三部中藍色的方塊,此時如果有線程正在遍歷藍色的方塊,對於這條線程而言完全讀到任何差異。線程依然是安全的。藍色的方塊只有當所有的線程都不再依賴的時候才會被垃圾回收期搜集。
(3)哈希擴容
哈希表極力降低沖突的概率,因此當哈希表容納的Entry過多時,會自動擴容。 哈希的擴容后的容量一定為原來的兩倍,因此只要哈希表的初始容量是2的整次冪(實際上就是如此),那么哈希表容量一直是2的整次冪。
ConcurrentHashMap使用了一種巧妙的方法,令哈希表即便是在擴容期間,也能保證無鎖的讀。
第一步將分配新的table,長度為原來的2倍。在遍歷原table的每一項,並對鏈表進行操作(此處將只演示對某一條鏈表的操作)。首先取出鏈表最某段的連續的一組節點,此組節點的hashCode在新table中對應的偏移位置(Index)是相同的。
(注:原來一個桶的Entry只可能被重新哈希到兩個位置)
再令新table中對應的偏移地址處指向之前選取組的首節點。
將該組節點的前趨全部復制,並各自通過哈希函數計算其在新table中的位置,並插入到該位置上。
這一步僅僅改變指向table的指針,導致原來的舊table以及被復制節點的本體變得不可達了。這些節點最終將被垃圾回收器回收。整個擴容過程即便耗時很長,但對於其他線程都是透明的。
轉自:
http://taozeyu.com/software/2014/03/14/concurrent-hash-source-code-analyze.html