集合是編程中最常用的數據結構。而談到并發,幾乎總是離不開集合這類高級數據結構的支持。比如兩個線程需要同時訪問一個中間臨界區(Queue),比如常會用緩存作為外部文件的副本(HashMap)。這篇文章主要分析jdk1.5的3種并發集合類型(concurrent,copyonright,queue)中的ConcurrentHashMap,讓我們從原理上細致的了解它們,能夠讓我們在深度項目開發中獲益非淺。
通過分析Hashtable就知道,synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨占,ConcurrentHashMap允許多個修改操作并發進行,其關鍵在于使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的hash table,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以并發進行。有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖。這里“按順序”是很重要的,否則極有可能出現死鎖,在ConcurrentHashMap內部,段數組是final的,并且其成員變量實際上也是final的,但是,僅僅是將數組聲明為final的并不保證數組成員也是final的,這需要實現上的保證。這可以確保不會出現死鎖,因為獲得鎖的順序是固定的。
一、結構解析
ConcurrentHashMap和Hashtable主要區別就是圍繞著鎖的粒度以及如何鎖,可以簡單理解成把一個大的HashTable分解成多個,形成了鎖分離。如圖:
而Hashtable的實現方式是---鎖整個hash表
二、應用場景
當有一個大數組時需要在多個線程共享時就可以考慮是否把它給分層多個節點了,避免大鎖。并可以考慮通過hash算法進行一些模塊定位。
其實不止用于線程,當設計數據表的事務時(事務某種意義上也是同步機制的體現),可以把一個表看成一個需要同步的數組,如果操作的表數據太多時就可以考慮事務分離了(這也是為什么要避免大表的出現),比如把數據進行字段拆分,水平分表等.
三、源碼解讀
ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關系
/** * The segments, each of which is a specialized hash table */ final Segment<K,V>[] segments;不變(Immutable)和易變(Volatile)ConcurrentHashMap完全允許多個讀操作并發進行,讀操作并不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間添加或刪除元素,讀操作不加鎖將得到不一致的數據。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,其結構如下所示:
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,這避免了加鎖。其它為了加快定位段以及段中hash槽的速度,每個段hash槽的的個數都是2^n,這使得通過位運算就可以定位段和段中hash槽的位置。當并發級別為默認值16時,也就是段的個數,hash值的高4位決定分配在哪個段中。但是我們也不要忘記《算法導論》給我們的教訓:hash槽的的個數不應該是 2^n,這可能導致hash槽分配不均,這需要對hash值重新再hash一次。(這段似乎有點多余了 )
這是定位段的方法:
1. final Segment<K,V> segmentFor(int hash) { 2. return segments[(hash >>> segmentShift) & segmentMask]; 3. }數據結構關于Hash表的基礎數據結構,這里不想做過多的探討。Hash表的一個很重要方面就是如何解決hash沖突,ConcurrentHashMap 和HashMap使用相同的方式,都是將hash值相同的節點放在一個hash鏈中。與HashMap不同的是,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)。下面是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主要是為了定位段,參見上面的segmentFor方法。每個Segment相當于一個子Hash表,它的數據成員如下:
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.
http://www.kuaimin.cn/zonghe/20170227/293598.html |
http://www.kuaimin.cn/zonghe/20170227/293597.html |
http://www.kuaimin.cn/zonghe/20170227/293590.html |
count用來統計該段數據的個數,它是volatile(volatile 變量使用指南),它用來協調修改和讀取操作,以保證讀取操作能夠讀取到幾乎最新的修改。協調方式是這樣的,每次修改操作做了結構上的改變,如增加/刪除節點(修改節點的值不算結構上的改變),都要寫count值,每次讀取操作開始都要讀取count的值。這利用了 java 5中對volatile語義的增強,對同一個volatile變量的寫和讀存在happens-before關系。modCount統計段結構改變的次數,主要是為了檢測對多個段進行遍歷過程中某個段是否發生改變,在講述跨段操作時會還會詳述。threashold用來表示需要進行rehash的界限值。table數組存儲段中節點,每個數組元素是個hash鏈,用HashEntry表示。table也是volatile,這使得能夠讀取到最新的 table值而不需要同步。loadFactor表示負載因子。
|
新聞熱點
疑難解答