ConcurrentHashMap 底層原理,你真的理解了嗎?

ConcurrentHashMap 是 HashMap 的線程安全版本,與之前版本的ConcurrentHashMap實現來看,java 8中做了較大調整,本文僅分析java 8的實現,java 8 之前的實現暫不做分析。

簡單介紹

爲了更好的導入本文,首先展示一下ConcurrentHashMap的結構,請看下面的圖片:
在這裏插入圖片描述
和 HashMap 一樣,ConcurrentHashMap 使用了一個 table 來存儲 Node,ConcurrentHashMap 同樣使用記錄的 key 的 hashCode來尋找記錄的存儲 index,而處理哈希衝突的方式與 HashMap 也是類似的,衝突的記錄將被存儲在同一位置上,形成一條鏈表,當鏈表長度大於 8 時,會將鏈表轉化爲紅黑樹。從而將查找的複雜度從 O(N) 降到 O(logN)。

ConcurrentHashMap 怎麼保證線程安全?

我們通過 ConcurrentHashMap 的 put 方法來講解 它怎麼實現線程安全。

put 方法實現步驟:

  1. 計算記錄的 key 的 hashCode ,然後計算 table 的 index 位置,獲取該 index 的值 x;
  2. 如果 x 爲 null,說明還沒有記錄,調用 CAS 操作 該新的記錄插入到table的index位置上去;
  3. 如果 x 不爲 null,通過 synchronized 關鍵字 對 table 的 index 位置加鎖;
  4. 然後判斷table的index位置上的第一個節點的hashCode值,如果hashCode值小於0,那麼就是一顆紅黑樹,如果不小於0,那麼就還是一條鏈表;
  5. 如果是一條鏈表,查找鏈表尋找是否有一個記錄的 key 值和本次插入的 key 值相同,相同則將替換掉 value 值;不同的話直接添加到鏈表中;
  6. 如果是一棵紅黑樹,調用 putTreeVal方法進行插入操作;
  7. 插入完成,判斷是不是更新操作,不是更新操作的話,size + 1。

注意

  • 第 2 步中,CAS操作 即compare and swap,是非阻塞的原子性操作,由UnSafe類提供。
  • 第 3 步中,當前線程只會鎖住table的index位置,其他位置上沒有鎖住,所以此時其他線程可以安全的獲得其他的table位置來進行操作。這也就提高了ConcurrentHashMap的併發度。
  • 第 7 步中,table的擴容操作也會在更新size的時候發生,如果在更新size之後發現table中的記錄數量達到了閾值,就需要進行擴容操作。

總結:

Java8 之後,ConcurrentHashMap 底層數據結構跟 HashMap 類似,都是基於 數組+鏈表+紅黑樹實現的,而 ConcurrentHashMap 之所以是線程安全的,是因爲它拋棄了 Hashtable 給整個方法加 synchronized 鎖的理念,而是讓 CAS操作 和 synchronized鎖 結合,在進行修改操作是,只對 table數組 的一項進行加鎖,其他數組項可以繼續被其他線程使用,這就提高了 ConcurrentHashMap 的併發度。
--------------------------------------------- 想看 put方法 的源碼?請往下看 ---------------------------------------------

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();//ConcurrentHashMap不存儲key/value爲null
    int hash = spread(key.hashCode());//計算key的hash值
    int binCount = 0;//桶中元素的大小,如果大於等於8,則旋轉爲紅黑樹
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;//f :計算出key在 hash中數組桶的鏈表/紅黑樹的根,n:桶的長度,i:key在數組的位置,fh:f的hash值
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();//如果數組爲0或者沒有元素,初始化
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//f指向數組中的值(鏈表/紅黑樹)
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))//如果f爲 null表示鏈表沒有值,則此次放入的key/value存入鏈表的根 (1)
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)//如果在進行擴容先進行擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {//此次添加key/value的鏈表有元素,則對鏈表/紅黑樹進行加鎖 這裏是java8和java7的不同之處 java8採用的加鎖桶,而不是一段桶
                if (tabAt(tab, i) == f) {//出現hash碰撞(情況1:線程1和線程2同時插入在上面(1) 由於是CAS操作只有一個線程會成功,第二個線程會進入到這一步 情況2:普通的hash碰撞),
                    if (fh >= 0) {//大於0表示桶是鏈表 TREEBIN   = -2 桶是紅黑樹
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;//鏈表中的一個元素的key
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {//如何hash和key都和已存在的元素相等則根據onlyIfAbsebt的值,確定是用之前的值還是新值覆蓋
                                oldVal = e.val;
                                if (!onlyIfAbsent)//如果onlyIfAbsent爲fasle,新值覆蓋老值
                                    e.val = value;
                                break;//退出,操作完成
                            }
                            Node<K,V> pred = e;//鏈表最末尾的值作爲新值的前一個元素
                            if ((e = e.next) == null) {//如果已經到了末尾值,則創建新的node存放此次插入的key/value
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    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) {//如果不等於0判斷是否需要旋轉爲紅黑樹
                if (binCount >= TREEIFY_THRESHOLD)//如果大於8則旋轉爲紅黑樹
                    treeifyBin(tab, i);//旋轉爲紅黑樹
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

明天的你,一定會感謝今天努力的自己!

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