JDK7源碼分析ConcurrentHashMap

一,的ConcurrentHashMap的由來


原因可以總結成以下兩點:

1,HashMap中是非線程安全的,在併發的場景中可能導致死循環

2,hasetable雖然線程安全但效率卻很低下


1>線程不安全的HashMap中

下面代碼取自併發編程藝術一書中,執行該代碼會引起死循環

public class HashMapTest {

   public static void main(String[] args) throws InterruptedException {
       final HashMap<String, String> map = new HashMap<String, String>(2);
       Thread t = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i < 10000; i++) {
                   new Thread(new Runnable() {
                        @Override
                       public void run() {
                           map.put(UUID.randomUUID().toString(), "");
                            System.out.println(Thread.currentThread().getName());
                       }
                   }, "ftf" + i).start();
               }
           }
       }, "ftf");
       t.start();
       t.join();
   }
}

頂部查看CPU使用率:


2>哈希表的效率低下

哈希表使用內置鎖同步來保證線程安全,在高併發的場景下,當線程1訪問hasetable的同步方法時,此時線程1正在執行放操作,其他線程此時既不能執行把操作也不能執行獲得操作,只能等待該線程釋放鎖,再競爭獲取鎖。


在jdk1.7中的ConcurrentHashMap採用鎖分段的技術來提升併發的訪問效率。簡單來說就是給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問從而提升了併發的訪問效率。


二,ConcurrentHashMap中的數據結構

的ConcurrentHashMap是由區隔數組結構和HashEntry數組結構組成,每個HashEntry是一個鏈表結構.SEGMENT繼承了的ReentrantLock,因此段是一種可重入鎖,在ConcurrentHashMap的裏扮演鎖的角色; HashEntry則用於存儲鍵值對數據。一個的ConcurrentHashMap裏包含一個段數組.SEGMENT的結構和HashMap中類似,是一種數組和鏈表結構。一個段裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個段守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的段鎖


段和HashEntry的源碼如下:

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

   transient volatile HashEntry<K,V>[] table;

   transient int threshold;

   final float loadFactor;

   Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
       this.loadFactor = lf;
       this.threshold = threshold;
       this.table = tab;
   }
   ...省略其他的代碼...
   ...只是爲了看Segment中維護了HashEntry數組...


static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;

    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ...省略其他的代碼...


三,ConcurrentHashMap中的構造函數

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel)
{
    //這就不用說了
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    //最大只能有65535個槽位即最大的併發級別爲65535
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    //ssize爲Segment的大小爲2的n次方,爲什麼是2的n次方,方便位運算,假設併發級別爲16,則sshift=4,ssize=16
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    //segmentShift和segmentMask後續會用到用於參與定位hase運算的位數
    this.segmentShift = 32 - sshift;
    //掩碼
    this.segmentMask = ssize - 1;
    //最大容量爲2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //每個槽位可以分多少個容量
    int c = initialCapacity / ssize;
    //這裏就很好理解了比如initialCapacity爲65,ssize爲16,則c爲4,不夠存65個的,則每個槽位多存一個就夠了
    if (c * ssize < initialCapacity)
        ++c;
    //這裏爲2的原因是向HashEntry中插入第一個元素不會擴容,第二個纔會擴容,在工作發現很多隻插入一個元素的情形,不至於浪費
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //這裏爲啥需要這個判斷呢,原因是假設每個槽位的的容量爲4,那麼最小的容量不足以存放所以擴大
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0],創建第一個segments[0]元素
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    //創建大小爲ssize的Segment數組
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    //這裏的意思是將segments[0]寫入到ss中
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    //將創建好的Segment數組賦值給全局變量segments
    this.segments = ss;
}


參數:initialCapacity:顧名思義初始化容量,表示的ConcurrentHashMap的初始化容量也就是HashEntry的數量,默認值爲16,最大值爲2的30次方


loadFactor:負載因子,默認值爲0.75f,當段內的HashEntry數組容量超過閾值,這個閾值等於loadFactor * cap,就需要rehash


concurrencyLevel:併發級別,默認值爲16,段數組的長度ssize是通過concurrencyLevel計算得出的,concurrencyLevel的最大值是65535,這意味着線段數組的長度最大爲65536.Segment的個數是大於等於concurrencyLevel的第一個2的ñ次方的數。比如,如果concurrencyLevel爲12,13,14,15,16這些數,則段的數目爲16(2的4次方)


四,把方法分析

public V put(K key, V value) {
   Segment<K,V> s;
   //不可以存null的值
   if (value == null)
       throw new NullPointerException();
   //計算key的hash值
   int hash = hash(key);
   //根據hash值找到Segment[]數組中j的位置
   int j = (hash >>> segmentShift) & segmentMask;
   //ensureSegment(j)對Segment[j]進行初始化,開始的時候只初始化了Segment[0]
   if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
        (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
       s = ensureSegment(j);
   //調用Segment的put方法存放數據
   return s.put(key, hash, value, false);
}

這裏可以得出兩點:

1,ConcurrentHashMap不能存空值

2,根據HASE值找到對應的段進而調用其內部的放方法存放數據

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
   //調用ReentrantLock的tryLock()獲取鎖,獲取失敗則進入scanAndLockForPut(key, hash, value);
   HashEntry<K,V> node = tryLock() ? null :
       scanAndLockForPut(key, hash, value);
   V oldValue;
   try {
       HashEntry<K,V>[] tab = table;
       //利用hash值找到當前K,V應該存放在HashEntry數組中的什麼位置
       int index = (tab.length - 1) & hash;
       //取鏈表的頭部
       HashEntry<K,V> first = entryAt(tab, index);
       for (HashEntry<K,V> e = first;;) {//
           if (e != null) {
               K k;
               //如果設置的key和鏈表中的key重複則覆蓋舊值
               if ((k = e.key) == key ||
                   (e.hash == hash && key.equals(k))) {
                   oldValue = e.value;
                   if (!onlyIfAbsent) {
                       e.value = value;
                       ++modCount;
                   }
                   break;
               }
               //這裏的作用所有的節點的put修改只能從頭部開始,一律添加到Hash鏈的頭部,最終e都會爲null然後進入else代碼中
               e = e.next;
           }
           else {
              //如果node不爲null則設置爲鏈表的表頭,如果爲null則初始化一個node並設置爲表頭
               if (node != null)
                   node.setNext(first);
               else
                   node = new HashEntry<K,V>(hash, key, value, first);
               int c = count + 1;
               //如果容量超過了segment的閾值則擴容
               if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                   rehash(node);
               else
                   //這裏會又set的操作其實是爲了上面的HashEntry<K,V> first = entryAt(tab, index);
                   //也就是將新的節點設置爲鏈表的頭部節點
                   setEntryAt(tab, index, node);
               ++modCount;
               count = c;
               oldValue = null;
               break;
           }
       }
   } finally {
       unlock();
   }
   return oldValue;
}

這裏先考慮當前線程獲取到鎖的情況:

1,根據散列值找到數組相應桶中的第一個鏈節點

2,遍歷數組,如果在節點中能找到密鑰相等的節點,則覆蓋舊值;如果沒有找到和密鑰相等的節點,並創建一個新的節點,並將該節點作爲鏈頭插入當前鏈

3,如果容量超過閾值(容量* loadFactor)並且數組長度沒有達到最大數組長度則翻版。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
   //獲取鏈表的頭部
   HashEntry<K,V> first = entryForHash(this, hash);
   HashEntry<K,V> e = first;
   HashEntry<K,V> node = null;
   int retries = -1; // negative while locating node
   //如果獲取到了鎖則返回null,否則進入循環
   while (!tryLock()) {
       HashEntry<K,V> f; // to recheck first below
       if (retries < 0) {
           if (e == null) {
               if (node == null) // speculatively create node
                   node = new HashEntry<K,V>(hash, key, value, null);
               retries = 0;
           }
           else if (key.equals(e.key))
               retries = 0;
           //如果當前線程嘗試了很多次沒有獲取到鎖,這裏的e值可能爲null也可能不爲空
           //如果走到這裏並且e.next不爲空這個時候tryLock()剛好獲取到鎖,則node爲null返回
           //如果走到這裏並且e.next爲空這個時候tryLock()沒有獲取到鎖,下一次循環則會構建一個新的node節點
           else
               e = e.next;
       }
       //MAX_SCAN_RETRIES:單核是1,多核是64,如果嘗試的次數大於MAX_SCAN_RETRIES,則調用lock()方法阻塞,使當前線程進入同步隊列中等待被喚醒
       else if (++retries > MAX_SCAN_RETRIES) {
           lock();
           break;
       }
       //如果這裏條件成立則說明該鏈表的表頭更新了,這個時候將設置爲-1並且這個時候也更新了嘗試獲取鎖的次數,循環又重新開始了
       else if ((retries & 1) == 0 &&
                (f = entryForHash(this, hash)) != first) {
           e = first = f; // re-traverse if entry changed
           retries = -1;
       }
   }
   return node;
}

這裏可以總結爲:

1,調用的ReentrantLock的的tryLock()獲取到鎖,循環結束

2,始終沒有獲取到鎖,調用阻塞方法進入同步隊列中等待被喚醒再次嘗試獲取鎖

3,節點的是否被實列化是獲取鎖的順序以及一堆條件判斷相結合的結果

private void rehash(HashEntry<K,V> node) {

    HashEntry<K,V>[] oldTable = table;
    //舊鏈表的長度
    int oldCapacity = oldTable.length;
    //新鏈表的容量*2
    int newCapacity = oldCapacity << 1;
    //Segment新的閾值
    threshold = (int)(newCapacity * loadFactor);
    //初始化新的鏈表
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    //掩碼
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            //計算舊錶中的數據在新表中存放的位置
            int idx = e.hash & sizeMask;
            if (next == null)   //  這裏成立的話,說明原先的舊錶中只有一個節點
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                //直到last.next==null的時候退出循環:找到鏈表中最後的一個元素
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                //將舊錶中的最後一個元素存放到新表中
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes,複製舊錶中剩餘的節點,去除舊錶中的最後一個節點
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    //將新設置的的k,v對應的node存放到鏈表的表頭
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

簡單來說:就是將segment數組中某個位置內部的HashEntry數組進行擴容2倍


五,GET方法分析

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    //計算key的hash值
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //通過hash值找到對應的Segment,遍歷查找到鏈表中對應key或者hash值則返回value值即可
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

根據散列值找到對應的segement,然後找到段對應的表中的具體位置HaseEntry鏈表,順着鏈表遍歷查找就OK了。


六,刪除方法分析

public V remove(Object key) {
        int hash = hash(key);
        Segment<K,V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
}

 

final V remove(Object key, int hash, Object value) {
    //這裏if語句塊的代碼就不贅述了
    if (!tryLock())
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        //當前segment所維護的鏈表
        HashEntry<K,V>[] tab = table;
        //確定key的hash值所在鏈表的索引位置
        int index = (tab.length - 1) & hash;
        //獲取鏈表的鏈頭節點
        HashEntry<K,V> e = entryAt(tab, index);
        //pred用來記錄待刪除節點的前一個節點
        HashEntry<K,V> pred = null;
        while (e != null) {
            K k;
            HashEntry<K,V> next = e.next;
            //當找到了待刪除節點
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) {
                V v = e.value;
                if (value == null || value == v || value.equals(v)) {
                    //如果待刪除節點的前節點爲null,即待刪除節點是鏈頭節點,此時把頭節點的next節點設置到頭節點的位置
                    if (pred == null)
                        setEntryAt(tab, index, next);
                    //如果有前節點,則待刪除節點的前節點的next指向待刪除節點的的下一個節點,刪除成功
                    else
                        pred.setNext(next);
                    ++modCount;
                    --count;
                    oldValue = v;
                }
                break;
            }
            pred = e;
            e = next;
        }
    } finally {
        unlock();
    }
    return oldValue;
}

去除方法可以歸納爲兩點:

如圖1所示,待刪除節點是頭結點,此時把頭節點的下一個節點設置到頭節點的位置,刪除成功

2,待刪除的節點不是頭結點,則待刪除節點的前節點的下一個指向待刪除節點的的下一個節點,刪除成功


到此jdk1.7的ConcurrentHashMap中的主要內容已經分析完畢,下篇將會介紹JDK1.8關於ConcurrentHashMap中的源碼分析。


參考文章:

Doug Lea:“Java併發編程實戰”

方騰飛,魏鵬,程曉明:“併發編程的藝術”


CSDN文章同步會慢些,歡迎關注微信公衆號:挨踢男孩


    



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章