標籤:extend always 開始 可變 key 不一致 情況 hash nts
一、為何用ConcurrentHashMap
在並發編程中使用HashMap可能會導致死迴圈,而使用安全執行緒的HashTable效率又低下。
線程不安全的HashMap
在多線程環境下,使用HashMap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在並發情況下不能使用HashMap
效率低下的HashTable
Hashtable使用synchronized來保證線程的安全,但是線上程競爭激烈的情況下Hashtable的效率非常低下。當一個線程訪問Hashtable的同步方法,其他方法訪問Hashtable的同步方法時,會進入阻塞或者輪詢狀態。如果線程1使用put進行元素添加,線程2不但不能用put方法添加於元素同是也無法用get方法來擷取元素,所以競爭越激烈效率越低。
ConcurrentHashMap的鎖分段技術
Hashtable容器在競爭激烈的並發環境效率低下的原因是所有訪問Hashtable的線程都必須競爭同一把鎖,假如容器有多把鎖,每一把鎖用於鎖住容器中一部分資料,那麼多線程訪問容器裡不同資料區段的資料時,線程間就不會存在鎖競爭,從而可以有效提高並發訪問率,這就是ConcurrentHashMap的鎖分段技術。將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個線程佔用鎖訪問其中一段資料的時候,其他段的資料也能被其他線程訪問。
二、結構解析
ConcurrentHashMap和Hashtable主要區別就是圍繞著鎖的粒度以及如何鎖,可以簡單理解成把一個大的Hashtable分解成多個,形成了鎖分離
感覺一個segment 就相當於一個 Hashtable
二、應用情境
當有一個大數組時需要在多個線程共用時就可以考慮是否把它給分層多個節點了,避免大鎖。並可以考慮通過hash演算法進行一些模組定位
其實不止用於線程,當設計資料表的事務時(事務事務某種意義上也是同步機制的體現),可以把一個表看成一個需要同步的數組,如果操作的表資料太多就可以考慮事務分離了(這也是為什麼要避免大表的出現),比如把資料進列欄位拆分、水平分表等
三、源碼解讀
從可以看出,ConcurrentHashMap內部分為很多個Segment,每一個Segment擁有一把鎖,然後每個Segment(繼承ReentrantLock)下麵包含很多個HashEntry列表資料。對於一個key,需要經過三次(為什麼要hash三次?後面解釋)hash操作,才能最終定位這個元素的位置,這三次hash分別為:
1.對於一個key,先進行一次hash操作,得到hash值h1,也即h1 = hash1(key)
2.將得到的h1的高几位進行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通過h2能夠確定該元素放在哪個Segment
3.將得到的h1進行第三次hash,得到hash值h3,也即h3=hash3(h1),確定h3能夠確定該元素放置在哪個HashEntry
註:在使用key定位Segment之前進行的那次hash操作,即第一次hash, 這次hash的主要目的是為了減少雜湊衝突,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率。假如雜湊的品質差到極點,那麼所有的元素都在一個Segment中,不僅存取元素緩慢,分段鎖也會失去意義
ConcurrentHashMap核心方法特點闡述
ConcurrentHashMap完全允許多個讀操作並發進行,讀操作並不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間添加或刪除元素,讀操作不加鎖將得到不一致的資料。ConcurrentHashMap的實現技術是保證 HashEntry幾乎是不可變的,為了確保讀操作能夠看到最新的值,將value設定成volatile;;下面是HashEntry的結構
1. static final class HashEntry<K,V> { 2. final K key; 3. final int hash; 4. volatile V value; 5. final HashEntry<K,V> next; 6. }
可以看到除了value 不是final的,其他值都是final的,這意味著不能從hash鏈的中間或尾部添加或刪除節點,因為這需要修改 next 的引用值,所有的節點的修改只能從頭部開始
對於put操作,可以一律添加到Hash鏈的頭部但是對於remove操作,可能需要從中間刪除一個節點,這就需要將要刪除節點的前面所有節點整個複製一遍,最後一個節點指向要刪除結點的下一個結點。這在講解刪除操作時還會詳述。為了確保讀操作能夠看到最新的值,將value設定成volatile,這避免了加鎖。
ConcurrentHashMap的資料成員及其作用闡述:
1 public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 2 implements ConcurrentMap<K, V>, Serializable { 3 /** 4 * Mask value for indexing into segments. The upper bits of a 5 * key‘s hash code are used to choose the segment. 6 */ 7 final int segmentMask; 8 9 /** 10 * Shift value for indexing within segments. 11 */ 12 final int segmentShift; 13 14 /** 15 * The segments, each of which is a specialized hash table 16 */ 17 final Segment<K,V>[] segments; 18 }
所有的成員都是final的,其中segmentMask和segmentShift主要是為了定位段
1. final Segment<K,V> segmentFor(int hash) { 2. return segments[(hash >>> segmentShift) & segmentMask]; 3. }
每個Segment相當於一個小的 Hashtable,它的資料成員如下:
1. static final class Segment<K,V> extends ReentrantLock implements Serializable { 2. private static final long serialVersionUID = 2249069246763182397L; 3. /** 4. * The number of elements in this segment‘s region. 5. */ 6. transient volatile int count; 7. 8. /** 9. * Number of updates that alter the size of the table. This is 10. * used during bulk-read methods to make sure they see a 11. * consistent snapshot: If modCounts change during a traversal 12. * of segments computing size or checking containsValue, then 13. * we might have an inconsistent view of state so (usually) 14. * must retry. 15. */ 16. transient int modCount; 17. 18. /** 19. * The table is rehashed when its size exceeds this threshold. 20. * (The value of this field is always <tt>(int)(capacity * 21. * loadFactor)</tt>.) 22. */ 23. transient int threshold; 24. 25. /** 26. * The per-segment table. 27. */ 28. transient volatile HashEntry<K,V>[] table; 29. 30. /** 31. * The load factor for the hash table. Even though this value 32. * is same for all segments, it is replicated to avoid needing 33. * links to outer object. 34. * @serial 35. */ 36. final float loadFactor; 37. }
count用來統計該段資料的個數,它是volatile,它用來協調修改和讀取操作,以保證讀取操作能夠讀取到幾乎最新的修改。協調方式是這樣的:每次修改操作做了結構上的改變,如增加/刪除節點(修改節點的值不算結構上的改變),都要寫count值,每次讀取操作之前都要讀取count的值
2.Java集合-ConcurrentHashMap實現原理及源碼分析