JUC之ConcurrentHashMap(八)

1、Hash表 

          1. 什麼是Hash表java

                 hash函數就是根據key計算出應該存儲地址的位置,而哈希表是基於哈希函數創建的一種查找表編程

          2.  hash函數設計的考慮因素數組

  •   計算散列地址所須要的時間(即hash函數自己不要太複雜)
  •   關鍵字的長度
  •   表長
  •   關鍵字分佈是否均勻,是否有規律可循
  •   設計的hash函數在知足以上條件的狀況下儘可能減小衝突

           3.哈希衝突的解決方案安全

              無論hash函數設計的如何巧妙,總會有特殊的key致使hash衝突,特別是對動態查找表來講。hash函數解決衝突的方法有如下幾個經常使用的方法數據結構

                    A.開放定製法(線性探索)
                    B.鏈地址法(HashMap)
                    C.公共溢出區法創建一個特殊存儲空間,專門存放衝突的數據。此種方法適用於數據和衝突較少的狀況。
                    D.再散列法(布隆過濾器)準備若干個hash函數,若是使用第一個hash函數發生了衝突,就使用第二個hash函數,第二個也衝突,使用第三個……     多線程

   開放定址法

       當一個關鍵字和另外一個關鍵字發生衝突時,使用某種探測技術在Hash表中造成一個探測序列,而後沿着這個探測序列依次查找下去,當碰到一個空的單元時,則插入其中。基本公式爲:hash(key) = (hash(key)+di)mod TableSize。其中di爲增量序列,TableSize爲表長。根據di的不一樣咱們又能夠分爲線性探測,平方(二次)探測,雙散列探測。 併發

 1)線性探測 
以增量序列 1,2,……,(TableSize -1)循環試探下一個存儲地址,即di = i。若是table[index+di]爲空則進行插入,反之試探下一個增量。可是線性探測也有弊端,就是會形成元素彙集現象,下降查找效率。具體例子以下圖: 函數

 

 

特別對於開放定址法的刪除操做,不能簡單的進行物理刪除,由於對於同義詞來講,這個地址可能在其查找路徑上,若物理刪除的話,會中斷查找路徑,故只能設置刪除標誌。高併發

//插入函數,利用線性探測法 
bool Insert_Linear_Probing(int num){
    //哈希表已經被裝滿,則不在填入 
    if(this->size == this->length){
        return false;
    }
    int index = this->hash(num);
    if(this->data[index] == MAX){
        this->data[index] = num;
    }else{
        int i = 1;
        //尋找合適位置 
        while(this->data[(index+i)%this->length] != MAX){
            i++;
        }
        index = (index+i)%this->length; 
        this->data[index] = num;
    }
    if(this->delete_flag[index] == 1){//以前設置爲刪除 
        this->delete_flag[index] = 0; 
    }
    this->size++;
    return true;
}

鏈地址法

 HashMap便是採用了鏈地址法,也就是數組+鏈表的方式,HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。  性能

//HashMap的主幹數組,能夠看到就是一個Entry數組,初始值爲空數組{},主幹數組的長度必定是2的次冪,至於爲何這麼作,後面會有詳細分析。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry是HashMap中的一個靜態內部類。代碼以下

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後獲得的值,存儲在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

因此,HashMap的總體結構以下  

 

 

 

 

 

 

 

 簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

二.ConcurrentHashMap

      ConcurrentHashMap是Java併發包中提供的一個線程安全且高效的HashMap實現,ConcurrentHashMap在併發編程的場景中使用頻率很是之高,下面咱們來分析下ConcurrentHashMap的實現原理,並對其實現原理進行分析 。

     衆所周知,哈希表是種很是高效,複雜度爲O(1)的數據結構,在Java開發中,咱們最多見到最頻繁使用的就是HashMap和HashTable,可是在線程競爭激烈的併發場景中使用都不夠合理。

    HashMap :先說HashMap,HashMap是線程不安全的,在併發環境下,可能會造成環狀鏈表(多線程擴容時可能形成),致使get操做時,cpu空轉,  因此,在併發環境中使用HashMap是很是危險的。

  HashTable : HashTable和HashMap的實現原理幾乎同樣,差異無非是1.HashTable不容許key和value爲null;2.HashTable是線程安全的。可是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put全部相關操做都是synchronized的,這至關於給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操做該對象,那其餘線程只能阻塞,至關於將全部的操做串行化,在競爭激烈的併發場景中性能就會很是差。

 

      HashTable性能差主要是因爲全部操做須要競爭同一把鎖,而若是容器中有多把鎖,每一把鎖鎖一段數據比喻[11],這樣在多線程訪問時不一樣段的數據時,就不會存在鎖競爭了,這樣即可以有效地提升併發效率。這就是ConcurrentHashMap所採用的"分段鎖"思想。java1.7後的CHM中把每一個數組叫Segment,每一個segment下面存的是默認16段的Hashhenery,Hashhenery解決充突是在Hashhenery下面掛載鏈表,咱們就畫圖說明下分段鎖

 

 

ConcurrentHashMap初始化時,計算出Segment數組的大小ssize和每一個SegmentHashEntry數組的大小cap,並初始化Segment數組的第一個元素;其中ssize大小爲2的冪次方默認爲16cap大小也是2的冪次方最小值爲2,最終結果根據初始化容量initialCapacity進行計算,計算過程以下

if (c * ssize < initialCapacity)
    ++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
    cap <<= 1;

由於Segment繼承了ReentrantLock,全部segment是線程安全的,可是在1.8中放棄了Segment分段鎖的設計,使用的是Node+CAS+Synchronized來保證線程安全性,並且這樣設計的好處是層級下降了,鎖的粒度更小了,能夠說是一種優化,比喻鎖的是2,那麼他鎖的就只是發生衝突的2下面的鏈表,而不像1.7樣,是鎖整個HashEntry;並且1.8中對鏈表的長度進行了優化,在1.7的鏈表中鏈表查詢的複雜度是O(n),可是在1.8中爲了解決這問題引入了紅黑樹,在1.8中當咱們鏈表長度大於8時而且數組長度大於64時,就會發生一個鏈表的轉換,會把單向鏈表轉換成紅黑樹。

 

 

  put操做

     在1.7 中當執行put方法插入數據的時候,根據key的hash值,在Segment數組中找到對應的位置若是當前位置沒有值,則經過CAS進行賦值,接着執行Segmentput方法經過加鎖機制插入數據;假若有線程AB同時執行相同Segmentput方法

線程A 執行tryLock方法成功獲取鎖,而後把HashEntry對象插入到相應位置

線程B 嘗試獲取鎖失敗,則執行scanAndLockForPut()方法,經過重複執行tryLock()方法嘗試獲取鎖

在多處理器環境重複64次,單處理器環境重複1次,當執行tryLock()方法的次數超過上限時,則執行lock()方法掛起線程B
 
當線程A執行完插入操做時,會經過unlock方法施放鎖,接着喚醒線程B繼續執行 

但在1.8 中執行put方法插入數據的時候,根據key的hash值在Node數組中找到相應的位置若是當前位置的 Node尚未初始化,則經過CAS插入數據

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    //若是當前位置的`Node`尚未初始化,則經過CAS插入數據
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

若是當前位置的Node已經有值,則對該節點加synchronized鎖,而後從該節點開始遍歷,直到插入新的節點或者更新新的節點  

if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}

若是當前節點是TreeBin類型,說明該節點下的鏈表已經進化成紅黑樹結構,則經過putTreeVal方法向紅黑樹中插入新的節點  

else if (f instanceof TreeBin) {
    Node<K,V> p;
    binCount = 2;
    if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
        oldVal = p.val;
        if (!onlyIfAbsent)
            p.val = value;
    }
}

若是binCount不爲0,說明put操做對數據產生了影響,若是當前鏈表的節點個數達到了8個,則經過treeifyBin方法將鏈表轉化爲紅黑樹  

相關文章
相關標籤/搜索