ConcurrentHashMap原理分析(不知道是哪一年的筆記,須要從新整理)

ConcurrentHashMap原理分析

HashTable是一個線程安全的類,它使用synchronnized來鎖住整張hash表來實現線程安全,即每次鎖住整張表讓線程獨佔。ConcurrentHashMap容許多個修改操做併發進行,其關鍵在於使用了鎖分離的技術。它使用了多個鎖來控制對hash表的不一樣部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不一樣的部分,每一個段其實就是一個小的Hashtable,他們有本身的鎖。只要多個修改操做發生在不一樣的段上,他們就能夠併發進行。html

 

有些方法須要跨段,好比size()和containsValue(),他們可能須要鎖定整個表而不只僅是某個段,這須要按順序鎖定全部段,操做完畢後,又按順序釋放全部段的鎖。這裏的「按順序」是很重要的,不然極有可能出現死鎖,在ConcurrentHashMap內部,段數組是final的,而且其成員變量實際上也是final的,可是,僅僅是將數組聲明爲final的並不保證數組成員也是final的,這須要實現上的保證。這能夠確保不會出現死鎖,由於得到鎖的順序是固定的。數組

 

實現原理:安全

ConcurrentHashMap使用分段鎖技術,將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問,可以實現真正的併發訪問。以下圖是ConcurrentHashMap的內部結構圖:多線程

從圖中能夠看到,ConcurrentHashMap內部分爲不少個Segment,每個Segment擁有一把鎖,而後每一個Segment(繼承ReentrantLock併發

 

static final class Segment<K,V> extends ReentrantLock implements Serializableapp

 

Segment繼承了ReentrantLock,代表每一個segment均可以當作一個鎖。(ReentrantLock前文已經提到,不瞭解的話就把當作synchronized的替代者吧)這樣對每一個segment中的數據須要同步操做的話都是使用每一個segment容器對象自身的鎖來實現。只有對全局須要改變時鎖定的是全部的segment。ssh

Segment下面包含不少個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。

ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖能夠看出之間的關係this

/** spa

* The segments, each of which is a specialized hash table

*/  

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;  

     volatile HashEntry<K,V> next;  

 }

在JDK 1.6中,HashEntry中的next指針也定義爲final,而且每次插入將新添加節點做爲鏈的頭節點(同HashMap實現),並且每次刪除一個節點時,會將刪除節點以前的全部節點 拷貝一份組成一個新的鏈,而將當前節點的上一個節點的next指向當前節點的下一個節點,從而在刪除之後 有兩條鏈存在,於是能夠保證即便在同一條鏈中,有一個線程在刪除,而另外一個線程在遍歷,它們都能工做良好,由於遍歷的線程能繼續使用原有的鏈。於是這種實現是一種更加細粒度的happens-before關係,即若是遍歷線程在刪除線程結束後開始,則它能看到刪除後的變化,若是它發生在刪除線程正在執行中間,則它會使用原有的鏈,而不會等到刪除線程結束後再執行,即看不到刪除線程的影響。若是這不符合你的需求,仍是乖乖的用Hashtable或HashMap的synchronized版本,Collections.synchronizedMap()作的包裝。

 

而HashMap中的Entry只有key是final的

 

 

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;

        Entry<K,V> next;

        int hash;

 

不變 模式(immutable)是多線程安全裏最簡單的一種保障方式。由於你拿他沒有辦法,想改變它也沒有機會。
不變模式主要經過final關鍵字來限定的。在JMM中final關鍵字還有特殊的語義。Final域使得確保初始化安全性(initialization safety)成爲可能,初始化安全性讓不可變形對象不須要同步就能自由地被訪問和共享。

初始化

先看看ConcurrentHashMap的初始化作了哪些事情,構造函數的源碼以下:

public ConcurrentHashMap(int initialCapacity,

                             float loadFactor, int concurrencyLevel) {

        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

            throw new IllegalArgumentException();

        if (concurrencyLevel > MAX_SEGMENTS)

            concurrencyLevel = MAX_SEGMENTS;

        // Find power-of-two sizes best matching arguments

        int sshift = 0;

        int ssize = 1;

        while (ssize < concurrencyLevel) {

            ++sshift;

            ssize <<= 1;

        }

        this.segmentShift = 32 - sshift;

        this.segmentMask = ssize - 1;

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        int c = initialCapacity / ssize;

        if (c * ssize < initialCapacity)

            ++c;

        int cap = MIN_SEGMENT_TABLE_CAPACITY;

        while (cap < c)

            cap <<= 1;

        // create segments and segments[0]

        Segment<K,V> s0 =

            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

                             (HashEntry<K,V>[])new HashEntry[cap]);

        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

        this.segments = ss;

}

傳入的參數有initialCapacity,loadFactor,concurrencyLevel這三個。

  • initialCapacity表示新建立的這個ConcurrentHashMap的初始容量,也就是上面的結構圖中的Entry數量。默認值爲static final int DEFAULT_INITIAL_CAPACITY = 16;
  • loadFactor表示負載因子,就是當ConcurrentHashMap中的元素個數大於loadFactor * 最大容量時就須要rehash,擴容。默認值爲static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • concurrencyLevel表示併發級別,這個值用來肯定Segment的個數,Segment的個數是大於等於concurrencyLevel的第一個2的n次方的數。好比,若是concurrencyLevel爲12,13,14,15,16這些數,則Segment的數目爲16(2的4次方)。默認值爲static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想狀況下ConcurrentHashMap的真正的併發訪問量可以達到concurrencyLevel,由於有concurrencyLevel個Segment,假若有concurrencyLevel個線程須要訪問Map,而且須要訪問的數據都剛好分別落在不一樣的Segment中,則這些線程可以無競爭地自由訪問(由於他們不須要競爭同一把鎖),達到同時訪問的效果。這也是爲何這個參數起名爲「併發級別」的緣由。

初始化的一些動做:

  1. 驗證參數的合法性,若是不合法,直接拋出異常。
  2. concurrencyLevel也就是Segment的個數不能超過規定的最大Segment的個數,默認值爲static final int MAX_SEGMENTS = 1 << 16;,若是超過這個值,設置爲這個值。
  3. 而後使用循環找到大於等於concurrencyLevel的第一個2的n次方的數ssize,這個數就是Segment數組的大小,並記錄一共向左按位移動的次數sshift,並令segmentShift = 32 - sshift,而且segmentMask的值等於ssize - 1,segmentMask的各個二進制位都爲1,目的是以後能夠經過key的hash值與這個值作&運算肯定Segment的索引。
  4. 檢查給的容量值是否大於容許的最大容量值,若是大於該值,設置爲該值。最大容量值爲static final int MAXIMUM_CAPACITY = 1 << 30;。
  5. 而後計算每一個Segment平均應該放置多少個元素,這個值c是向上取整的值。好比初始容量爲15,Segment個數爲4,則每一個Segment平均須要放置4個元素。
  6. 最後建立一個Segment實例,將其當作Segment數組的第一個元素。

 

 

http://www.importnew.com/22007.html

https://my.oschina.net/hosee/blog/639352

相關文章
相關標籤/搜索