ConcurrentHashMap 原理分析

瞭解ConcurrentHashMap 實現原理,建議首先了解下HashMap實現原理。
HashMap 源碼解析(JDK1.8)

爲什麼要用ConcurrentHashMap

HashMap線程不安全,而Hashtable是線程安全,但是它使用了synchronized進行方法同步,插入、讀取數據都使用了synchronized,當插入數據的時候不能進行讀取(相當於把整個Hashtable都鎖住了,全表鎖),當多線程併發的情況下,都要競爭同一把鎖,導致效率極其低下。而在JDK1.5後爲了改進Hashtable的痛點,ConcurrentHashMap應運而生。

ConcurrentHashMap爲什麼高效?

JDK1.5中的實現

ConcurrentHashMap使用的是分段鎖技術,將ConcurrentHashMap將鎖一段一段的存儲,然後給每一段數據配一把鎖(segment),當一個線程佔用一把鎖(segment)訪問其中一段數據的時候,其他段的數據也能被其它的線程訪問,默認分配16個segment。默認比Hashtable效率提高16倍。

ConcurrentHashMap的結構圖如下(網友貢獻的圖,哈):

 

Paste_Image.png

JDK1.8中的實現

ConcurrentHashMap取消了segment分段鎖,而採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹
synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提升N倍。

JDK1.8的ConcurrentHashMap的結構圖如下:

Paste_Image.png

TreeBin: 紅黑二叉樹節點
Node: 鏈表節點

ConcurrentHashMap 源碼分析

ConcurrentHashMap 類結構參照HashMap,這裏列出HashMap沒有的幾個屬性。

 

/**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     hash表初始化或擴容時的一個控制位標識量。
     負數代表正在進行初始化或擴容操作
     -1代表正在初始化
     -N 表示有N-1個線程正在進行擴容操作
     正數或0代表hash表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小
     */
    private transient volatile int sizeCtl; 
    // 以下兩個是用來控制擴容的時候 單線程進入的變量
    /**
     * The number of bits used for generation stamp in sizeCtl.
     * Must be at least 6 for 32bit arrays.
     */
    private static int RESIZE_STAMP_BITS = 16;
    /**
     * The bit shift for recording size stamp in sizeCtl.
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    
    
    /*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // hash值是-1,表示這是一個forwardNode節點
    static final int TREEBIN   = -2; // hash值是-2  表示這時一個TreeBin節點

分析代碼主要目的:分析是如果利用CAS和Synchronized進行高效的同步更新數據。
下面插入數據源碼:

 

public V put(K key, V value) {
    return putVal(key, value, false);
}

    /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //ConcurrentHashMap 不允許插入null鍵,HashMap允許插入一個null鍵
    if (key == null || value == null) throw new NullPointerException();
    //計算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    //for循環的作用:因爲更新元素是使用CAS機制更新,需要不斷的失敗重試,直到成功爲止。
    for (Node<K,V>[] tab = table;;) {
        // f:鏈表或紅黑二叉樹頭結點,向鏈表中添加元素時,需要synchronized獲取f的鎖。
        Node<K,V> f; int n, i, fh;
        //判斷Node[]數組是否初始化,沒有則進行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //通過hash定位Node[]數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //檢查到內部正在移動元素(Node[] 數組擴容)
        else if ((fh = f.hash) == MOVED)
            //幫助它擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //鎖住鏈表或紅黑二叉樹的頭結點
            synchronized (f) {
                //判斷f是否是鏈表的頭結點
                if (tabAt(tab, i) == f) {
                    //如果fh>=0 是鏈表節點
                    if (fh >= 0) {
                        binCount = 1;
                        //遍歷鏈表所有節點
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果節點存在,則更新value
                            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是紅黑二叉樹節點
                    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;
                        }
                    }
                }
            }
            
            if (binCount != 0) {
                //如果鏈表長度已經達到臨界值8 就需要把鏈表轉換爲樹結構
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //將當前ConcurrentHashMap的size數量+1
    addCount(1L, binCount);
    return null;
}
  1. 判斷Node[]數組是否初始化,沒有則進行初始化操作
  2. 通過hash定位Node[]數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
  3. 檢查到內部正在擴容,如果正在擴容,就幫助它一塊擴容。
  4. 如果f!=null,則使用synchronized鎖住f元素(鏈表/紅黑二叉樹的頭元素)
    4.1 如果是Node(鏈表結構)則執行鏈表的添加操作。
    4.2 如果是TreeNode(樹型結果)則執行樹添加操作。
  5. 判斷鏈表長度已經達到臨界值8 就需要把鏈表轉換爲樹結構。

總結:
    JDK8中的實現也是鎖分離的思想,它把鎖分的比segment(JDK1.5)更細一些,只要hash不衝突,就不會出現併發獲得鎖的情況。它首先使用無鎖操作CAS插入頭結點,如果插入失敗,說明已經有別的線程插入頭結點了,再次循環進行操作。如果頭結點已經存在,則通過synchronized獲得頭結點鎖,進行後續的操作。性能比segment分段鎖又再次提升。



 

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