10分鐘掌握ConcurrentHashMap 3分鐘清楚和HashMap、Hashtable的區別

薛8發表於2019-03-10
image

前言

ConcurrentHashMap顧名思義就是同步的HashMap,也就是執行緒安全的HashMap,所以本篇介紹的ConcurrentHashMap和HashMap有著很重要的關係,所以建議之前沒有了解過HashMap的可以先看看這篇關於HashMap的原理分析《HashMap從認識到原始碼分析》,本篇繼續以JDK1.8版本的原始碼進行分析,最後在介紹完ConcurrentHashMap之後會對ConcurrentHashMap、Hashtable和HashMap做一個比較和總結。

ConcurrentHashMap

我們先看一下ConcurrentHashMap實現了哪些介面、繼承了哪些類,對ConcurrentHashMap有一個整體認知。

10分鐘掌握ConcurrentHashMap 3分鐘清楚和HashMap、Hashtable的區別
ConcurrentHashMap繼承AbstractMap介面,這個和HashMap一樣,然後實現了ConcurrentMap介面,這個和HashMap不一樣,HashMap是直接實現的Map介面。
再細看ConcurrentHashMap的結構,這裡列舉幾個重要的成員變數tablenextTablebaseCountsizeCtltransferIndexcellsBusy

  • table:資料型別是Node陣列,這裡的Node和HashMap的Node一樣都是內部類且實現了Map.Entry介面
  • nextTable:雜湊表擴容時生成的資料,陣列為擴容前的2倍
  • sizeCtl:多個執行緒的共享變數,是操作的控制識別符號,它的作用不僅包括threshold的作用,在不同的地方有不同的值也有不同的用途
    • -1代表正在初始化
    • -N代表有N-1個執行緒正在進行擴容操作
    • 0代表hash表還沒有被初始化
    • 正數表示下一次進行擴容的容量大小
  • ForwardingNode:一個特殊的Node節點,Hash地址為-1,儲存著nextTable的引用,只有table發生擴用的時候,ForwardingNode才會發揮作用,作為一個佔位符放在table中表示當前節點為null或者已被移動
    10分鐘掌握ConcurrentHashMap 3分鐘清楚和HashMap、Hashtable的區別
    ConcurrentHashMapHashMap一樣都是採用拉鍊法處理雜湊衝突,且都為了防止單連結串列過長影響查詢效率,所以當連結串列長度超過某一個值時候將用紅黑樹代替連結串列進行儲存,採用了陣列+連結串列+紅黑樹的結構
    10分鐘掌握ConcurrentHashMap 3分鐘清楚和HashMap、Hashtable的區別
    所以從結構上看HashMapConcurrentHashMap還是很相似的,只是ConcurrentHashMap在某些操作上採用了CAS + synchronized來保證併發情況下的安全。
    說到ConcurrentHashMap處理併發情況下的執行緒安全問題,這不得不提到Hashtable,因為Hashtable也是執行緒安全的,那ConcurrentHashMapHashtable有什麼區別或者有什麼高明之處嘛?以至於官方都推薦使用ConcurrentHashMap來代替Hashtable
  • 執行緒安全的實現Hashtable採用物件鎖(synchronized修飾物件方法)來保證執行緒安全,也就是一個Hashtable物件只有一把鎖,如果執行緒1拿了物件A的鎖進行有synchronized修飾的put方法,其他執行緒是無法操作物件A中有synchronized修飾的方法的(如get方法、remove方法等),競爭激烈所以效率低下。而ConcurrentHashMap採用CAS + synchronized來保證併發安全性,且synchronized關鍵字不是用在方法上而是用在了具體的物件上,實現了更小粒度的鎖,等會原始碼分析的時候在細說這個SUN大師們的鬼斧神工
  • 資料結構的實現:Hashtable採用的是陣列 + 連結串列,當連結串列過長會影響查詢效率,而ConcurrentHashMap採用陣列 + 連結串列 + 紅黑樹,當連結串列長度超過某一個值,則將連結串列轉成紅黑樹,提高查詢效率。

建構函式

ConcurrentHashMap的建構函式有5個,從數量上看就和HashMapHashtable(4個)的不同,多出的那個建構函式是public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel),即除了傳入容量大小、負載因子之外還多傳入了一個整型的concurrencyLevel,這個整型是我們預先估計的併發量,比如我們估計併發是30,那麼就可以傳入30
其他的4個建構函式的引數和HashMap的一樣,而具體的初始化過程卻又不相同,HashMapHashtable傳入的容量大小和負載因子都是為了計算出初始閾值(threshold),而ConcurrentHashMap傳入的容量大小和負載因子是為了計算出sizeCtl用於初始化table,這個sizeCtl即table陣列的大小,不同的建構函式計算sizeCtl方法都不一樣。

//無參建構函式,什麼也不做,table的初始化放在了第一次插入資料時,預設容量大小是16和HashMap的一樣,預設sizeCtl為0
public ConcurrentHashMap() {
}

//傳入容量大小的建構函式。
public ConcurrentHashMap(int initialCapacity) {
    //如果傳入的容量大小小於0 則丟擲異常。
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    //如果傳入的容量大小大於允許的最大容量值 則cap取允許的容量最大值 否則cap =
    //((傳入的容量大小 + 傳入的容量大小無符號右移1位 + 1)的結果向上取最近的2冪次方),
    //即如果傳入的容量大小是12 則 cap = 32(12 + (12 >>> 1) + 1=19
    //向上取2的冪次方即32),這裡為啥一定要是2的冪次方,原因和HashMap的threshold一樣,都是為
    //了讓位運算和取模運算的結果一樣。
    //MAXIMUM_CAPACITY即允許的最大容量值 為2^30。
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               //tableSizeFor這個函式即實現了將一個整數取2的冪次方。
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    //將上面計算出的cap 賦值給sizeCtl,注意此時sizeCtl為正數,代表進行擴容的容量大小。
    this.sizeCtl = cap;
}

//包含指定Map的建構函式。
//置sizeCtl為預設容量大小 即16。
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

//傳入容量大小和負載因子的建構函式。
//預設併發數大小是1。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

//傳入容量大小、負載因子和併發數大小的建構函式
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    //如果傳入的容量大小 小於 傳入的併發數大小,
    //則容量大小取併發數大小,這樣做的原因是確保每一個Node只會分配給一個執行緒,而一個執行緒則
    //可以分配到多個Node,比如當容量大小為64,併發數大
    //小為16時,則每個執行緒分配到4個Node。
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    //size = 1.0 + (long)initialCapacity / loadFactor 這裡計算方法和上面的建構函式不一樣。
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    //如果size大於允許的最大容量值則 sizeCtl = 允許的最大容量值 否則 sizeCtl =
    //size取2的冪次方。
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}
複製程式碼

put方法

  1. 判斷鍵值是否為null,為null丟擲異常。
  2. 呼叫spread()方法計算key的hashCode()獲得雜湊地址,這個HashMap相似。
  3. 如果當前table為空,則初始化table,需要注意的是這裡並沒有加synchronized,也就是允許多個執行緒去嘗試初始化table,但是在初始化函式裡面使用了CAS保證只有一個執行緒去執行初始化過程。
  4. 使用 容量大小-1 & 雜湊地址 計算出待插入鍵值的下標,如果該下標上的bucket為null,則直接呼叫實現CAS原子性操作的casTabAt()方法將節點插入到table中,如果插入成功則完成put操作,結束返回。插入失敗(被別的執行緒搶先插入了)則繼續往下執行。
  5. 如果該下標上的節點(頭節點)的雜湊地址為-1,代表需要擴容,該執行緒執行helpTransfer()方法協助擴容。
  6. 如果該下標上的bucket不為空,且又不需要擴容,則進入到bucket中,同時鎖住這個bucket,注意只是鎖住該下標上的bucket而已,其他的bucket並未加鎖,其他執行緒仍然可以操作其他未上鎖的bucket,這個就是ConcurrentHashMap為什麼高效的原因之一。
  7. 進入到bucket裡面,首先判斷這個bucket儲存的是紅黑樹(雜湊地址小於0,原因後面分析)還是連結串列。
  8. 如果是連結串列,則遍歷連結串列看看是否有雜湊地址和鍵key相同的節點,有的話則根據傳入的引數進行覆蓋或者不覆蓋,沒有找到相同的節點的話則將新增的節點插入到連結串列尾部。如果是紅黑樹,則將節點插入。到這裡結束加鎖
  9. 最後判斷該bucket上的連結串列長度是否大於連結串列轉紅黑樹的閾值(8),大於則呼叫treeifyBin()方法將連結串列轉成紅黑樹,以免連結串列過長影響效率。
  10. 呼叫addCount()方法,作用是將ConcurrentHashMap的鍵值對數量+1,還有另一個作用是檢查ConcurrentHashMap是否需要擴容。
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //不允許鍵值為null,這點與執行緒安全的Hashtable保持一致,和HashMap不同。
    if (key == null || value == null) throw new NullPointerException();
    //取鍵key的hashCode()和HashMap、Hashtable都一樣,然後再執行spread()方法計算得到雜湊地
    //址,這個spread()方法和HashMap的hash()方法一樣,都是將hashCode()做無符號右移16位,只不
    //過spread()加多了 &0x7fffffff,讓結果為正數。
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果table陣列為空或者長度為0(未初始化),則呼叫initTable()初始化table,初始化函式
        //下面介紹。
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //呼叫實現了CAS原子性操作的tabAt方法
        //tabAt方法的第一個引數是Node陣列的引用,第二個引數在Node陣列的下標,實現的是在Nod
        //e陣列中查詢指定下標的Node,如果找到則返回該Node節點(連結串列頭節點),否則返回null,
        //這裡的i = (n - 1)&hash即是計算待插入的節點在table的下標,即table容量-1的結果和哈
        //希地址做與運算,和HashMap的演算法一樣。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //如果該下標上並沒有節點(即連結串列為空),則直接呼叫實現了CAS原子性操作的
            //casTable()方法,
            //casTable()方法的第一個引數是Node陣列的引用,第二個引數是待操作的下標,第三
            //個引數是期望值,第四個引數是待操作的Node節點,實現的是將Node陣列下標為引數二
            //的節點替換成引數四的節點,如果期望值和實際值不符返回false,否則引數四的節點成
            //功替換上去,返回ture,即插入成功。注意這裡:如果插入成功了則跳出for迴圈,插入
            //失敗的話(其他執行緒搶先插入了),那麼會執行到下面的程式碼。
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //如果該下標上的節點的雜湊地址為-1(即連結串列的頭節點為ForwardingNode節點),則表示
        //table需要擴容,值得注意的是ConcurrentHashMap初始化和擴容不是用同一個方法,而
        //HashMap和Hashtable都是用同一個方法,當前執行緒會去協助擴容,擴容過程後面介紹。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //如果該下標上的節點既不是空也不是需要擴容,則表示這個連結串列可以插入值,將進入到連結串列
        //中,將新節點插入或者覆蓋舊值。
        else {
            V oldVal = null;
            //通過關鍵字synchroized對該下標上的節點加鎖(相當於鎖住鎖住
            //該下標上的連結串列),其他下標上的節點並沒有加鎖,所以其他執行緒
            //可以安全的獲得其他下標上的連結串列進行操作,也正是因為這個所
            //以提高了ConcurrentHashMap的效率,提高了併發度。
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //如果該下標上的節點的雜湊地址大於等於0,則表示這是
                    //個連結串列。
                    if (fh >= 0) {
                        binCount = 1;
                        //遍歷連結串列。
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果雜湊地址、鍵key相同 或者 鍵key不為空
                            //且鍵key相同,則表示存在鍵key和待插入的鍵
                            //key相同,則執行更新值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;
                            //如果找到了連結串列的最後一個節點都沒有找到相
                            //同鍵Key的,則是插入操作,將插入的鍵值新建
                            //個節點並且新增到連結串列尾部,這個和HashMap一
                            //樣都是插入到尾部。
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //如果該下標上的節點的雜湊地址小於0 且為樹節點
                    //則將帶插入鍵值新增到紅黑樹
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        //如果插入的結果不為null,則表示為替換
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash,
                        key,value)) != null){
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //判斷連結串列的長度是否大於等於連結串列的閾值(8),大於則將連結串列轉成
            //紅黑樹,提高效率。這點和HashMap一樣。
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}
複製程式碼

get方法

  1. 呼叫spread()方法計算key的hashCode()獲得雜湊地址。
  2. 計算出鍵key所在的下標,演算法是(n - 1) & h,如果table不為空,且下標上的bucket不為空,則到bucket中查詢。
  3. 如果bucket的頭節點的雜湊地址小於0,則代表這個bucket儲存的是紅黑樹,否則是連結串列。
  4. 到紅黑樹或者連結串列中查詢,找到則返回該鍵key的值,找不到則返回null。
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //運用鍵key的hashCode()計算出雜湊地址
    int h = spread(key.hashCode());
    //如果table不為空 且 table長度大於0 且 計算出的下標上bucket不為空,
    //則代表這個bucket存在,進入到bucket中查詢,
    //其中(n - 1) & h為計算出鍵key相對應的陣列下標的演算法。
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //如果雜湊地址、鍵key相同則表示查詢到,返回value,這裡查詢到的是頭節點。
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果bucket頭節點的雜湊地址小於0,則代表bucket為紅黑樹,在紅黑樹中查詢。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //如果bucket頭節點的雜湊地址不小於0,則代表bucket為連結串列,遍歷連結串列,在連結串列中查詢。
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
複製程式碼

remove方法

  1. 呼叫spread()方法計算出鍵key的雜湊地址。
  2. 計算出鍵key所在的陣列下標,如果table為空或者bucket為空,則返回null
  3. 判斷當前table是否正在擴容,如果在擴容則呼叫helpTransfer方法協助擴容。
  4. 如果table和bucket都不為空,table也不處於在擴容狀態,則鎖住當前bucket,對bucket進行操作。
  5. 根據bucket的頭結點判斷bucket是連結串列還是紅黑樹。
  6. 在連結串列或者紅黑樹中移除雜湊地址、鍵key相同的節點。
  7. 呼叫addCount方法,將當前table儲存的鍵值對數量-1。
public V remove(Object key) {
    return replaceNode(key, null, null);
}
    
final V replaceNode(Object key, V value, Object cv) {
    //計算需要移除的鍵key的雜湊地址。
    int hash = spread(key.hashCode());
    //遍歷table。
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //table為空,或者鍵key所在的bucket為空,則跳出迴圈返回。
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        //如果當前table正在擴容,則呼叫helpTransfer方法,去協助擴容。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            //將鍵key所在的bucket加鎖。
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //bucket頭節點的雜湊地址大於等於0,為連結串列。
                    if (fh >= 0) {
                        validated = true;
                        //遍歷連結串列。
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            //找到雜湊地址、鍵key相同的節點,進行移除。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    //如果bucket的頭節點小於0,即為紅黑樹。
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        //找到節點,並且移除。
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            //呼叫addCount方法,將當前ConcurrentHashMap儲存的鍵值對數量-1。
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}
複製程式碼

initTable初始化方法

table的初始化主要由initTable()方法實現的,initTable()方法初始化一個合適大小的陣列,然後設定sizeCtl。 我們知道ConcurrentHashMap是執行緒安全的,即支援多執行緒的,那麼一開始很多個執行緒同時執行put()方法,而table又沒初始化,那麼就會很多個執行緒會去執行initTable()方法嘗試初始化table,而put方法和initTable方法都是沒有加鎖的(synchronize),那SUN的大師們是怎麼保證執行緒安全的呢? 通過原始碼可以看得出,table的初始化只能由一個執行緒完成,但是每個執行緒都可以爭搶去初始化table。

  1. 判斷table是否為null,即需不需要首次初始化,如果某個執行緒進到這個方法後,其他執行緒已經將table初始化好了,那麼該執行緒結束該方法返回。
  2. 如果table為null,進入到while迴圈,如果sizeCtl小於0(其他執行緒正在對table初始化),那麼該執行緒呼叫Thread.yield()掛起該執行緒,讓出CPU時間,該執行緒也從執行態轉成就緒態,等該執行緒從就緒態轉成執行態的時候,別的執行緒已經table初始化好了,那麼該執行緒結束while迴圈,結束初始化方法返回。如果從就緒態轉成執行態後,table仍然為null,則繼續while迴圈。
  3. 如果table為nullsizeCtl不小於0,則呼叫實現CAS原子性操作的compareAndSwap()方法將sizeCtl設定成-1,告訴別的執行緒我正在初始化table,這樣別的執行緒無法對table進行初始化。如果設定成功,則再次判斷table是否為空,不為空則初始化table,容量大小為預設的容量大小(16),或者為sizeCtl。其中sizeCtl的初始化是在建構函式中進行的,sizeCtl = ((傳入的容量大小 + 傳入的容量大小無符號右移1位 + 1)的結果向上取最近的2冪次方)
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //如果table為null或者長度為0, //則一直迴圈試圖初始化table(如果某一時刻別的執行緒將table初始化好了,那table不為null,該//執行緒就結束while迴圈)。
    while ((tab = table) == null || tab.length == 0) {
        //如果sizeCtl小於0,
        //即有其他執行緒正在初始化或者擴容,執行Thread.yield()將當前執行緒掛起,讓出CPU時間,
        //該執行緒從執行態轉成就緒態。
        //如果該執行緒從就緒態轉成執行態了,此時table可能已被別的執行緒初始化完成,table不為
        //null,該執行緒結束while迴圈。
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //如果此時sizeCtl不小於0,即沒有別的執行緒在做table初始化和擴容操作,
        //那麼該執行緒就會呼叫Unsafe的CAS操作compareAndSwapInt嘗試將sizeCtl的值修改成
        //-1(sizeCtl=-1表示table正在初始化,別的執行緒如果也進入了initTable方法則會執行
        //Thread.yield()將它的執行緒掛起 讓出CPU時間),
        //如果compareAndSwapInt將sizeCtl=-1設定成功 則進入if裡面,否則繼續while迴圈。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //再次確認當前table為null即還未初始化,這個判斷不能少。
                if ((tab = table) == null || tab.length == 0) {
                    //如果sc(sizeCtl)大於0,則n=sc,否則n=預設的容量大
                    小16,
                    //這裡的sc=sizeCtl=0,即如果在建構函式沒有指定容量
                    大小,
                    //否則使用了有引數的建構函式,sc=sizeCtl=指定的容量大小。
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //建立指定容量的Node陣列(table)。
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //計算閾值,n - (n >>> 2) = 0.75n當ConcurrentHashMap儲存的鍵值對數量
                    //大於這個閾值,就會發生擴容。
                    //這裡的0.75相當於HashMap的預設負載因子,可以發現HashMap、Hashtable如果
                    //使用傳入了負載因子的建構函式初始化的話,那麼每次擴容,新閾值都是=新容
                    //量 * 負載因子,而ConcurrentHashMap不管使用的哪一種建構函式初始化,
                    //新閾值都是=新容量 * 0.75。
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
複製程式碼

transfer擴容方法

transfer()方法為ConcurrentHashMap擴容操作的核心方法。由於ConcurrentHashMap支援多執行緒擴容,而且也沒有進行加鎖,所以實現會變得有點兒複雜。整個擴容操作分為兩步:

  1. 構建一個nextTable,其大小為原來大小的兩倍,這個步驟是在單執行緒環境下完成的
  2. 將原來table裡面的內容複製到nextTable中,這個步驟是允許多執行緒操作的,所以效能得到提升,減少了擴容的時間消耗。
//協助擴容方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    //如果當前table不為null 且 f為ForwardingNode節點 且 //新的table即nextTable存在的情況下才能協助擴容,該方法的作用是讓執行緒參與擴容的複製。
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            //更新sizeCtl的值,+1,代表新增一個執行緒參與擴容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

//擴容的方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //根據伺服器CPU數量來決定每個執行緒負責的bucket數量,避免因為擴容的執行緒過多反而影響效能。
    //如果CPU數量為1,則stride=1,否則將需要遷移的bucket數量(table大小)除以CPU數量,平分給
    //各個執行緒,但是如果每個執行緒負責的bucket數量小於限制的最小是(16)的話,則強制給每個執行緒
    //分配16個bucket數。
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    //如果nextTable還未初始化,則初始化nextTable,這個初始化和iniTable初始化一樣,只能由
    //一個執行緒完成。
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    //分配任務和控制當前執行緒的任務進度,這部分是transfer()的核心邏輯,描述瞭如何與其他線
    //程協同工作。
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        //遷移過程(對當前指向的bucket),這部分的邏輯與HashMap類似,拿舊陣列的容量當做一
        //個掩碼,然後與節點的hash進行與操作,可以得出該節點的新增有效位,如果新增有效位為
        //0就放入一個連結串列A,如果為1就放入另一個連結串列B,連結串列A在新陣列中的位置不變(跟在舊數
        //組的索引一致),連結串列B在新陣列中的位置為原索引加上舊陣列容量。
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
複製程式碼

addCount、sumCount方法

addCount()做的工作是更新table的size,也就是table儲存的鍵值對數量,在使用put()remove()方法的時候都會在執行成功之後呼叫addCount()來更新table的size。對於ConcurrentHashMap來說,它到底有儲存有多少個鍵值對,誰也不知道,因為他是支援併發的,儲存的數量無時無刻都在變化著,所以說ConcurrentHashMap也只是統計一個大概的值,為了統計出這個值也是大費周章才統計出來的。

10分鐘掌握ConcurrentHashMap 3分鐘清楚和HashMap、Hashtable的區別

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //如果計算盒子不是空,或者修改baseCount的值+x失敗,則放棄對baseCount的修改。
    //這裡的大概意思就是首先嚐試直接修改baseCount,達到計數的目的,如果修改baseCount失敗(
    //多個執行緒同時修改,則失敗)
    //則使用CounterCell陣列來達到計數的目的。
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        //如果計數盒子是空的 或者隨機取餘一個陣列為空 或者修改這個槽位的變數失敗,
        //即表示出現了併發,則執行fullAddCount()方法進行死迴圈插入,同時返回,
        //否則代表修改這個槽位的變數成功了,繼續往下執行,不進入if。
        //每個執行緒都會通過ThreadLocalRandom.getProbe() & m定址找到屬於它的CounterCell,
        //然後進行計數。ThreadLocalRandom是一個執行緒私有的偽隨機數生成器,
        //每個執行緒的probe都是不同的。CounterCell陣列的大小永遠是一個2的n次方,初始容量
        //為2,每次擴容的新容量都是之前容量乘以二,處於效能考慮,它的最大容量上限是機器
        //的CPU數量,所以說CounterCell陣列的碰撞衝突是很嚴重的。
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
             //併發過大,使用CAS修改CounterCell失敗時候執行fullAddCount,
            fullAddCount(x, uncontended);
            return;
        }
        //如果上面對盒子的賦值成功,且check<=1,則直接返回,否則呼叫sumConut()方法計算
        if (check <= 1)
            return;
        s = sumCount();
    }
    //如果check>=0,則檢查是否需要擴容。
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
複製程式碼

size、mappingCount方法

sizemappingCount方法都是用來統計table的size的,這兩者不同的地方在size返回的是一個int型別,即可以表示size的範圍是[-2^31,2^31-1],超過這個範圍就返回int能表示的最大值,mappingCount返回的是一個long型別,即可以表示size的範圍是[-2^63,2^63-1]。
這兩個方法都是呼叫的sumCount()方法實現統計。

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
    
public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // ignore transient negative values
}
複製程式碼

HashMap、Hashtable、ConcurrentHashMap三者對比

\ HashMap Hashtable ConcurrentHashMap
是否執行緒安全
執行緒安全採用的方式 採用synchronized類鎖,效率低 採用CAS + synchronized,鎖住的只有當前操作的bucket,不影響其他執行緒對其他bucket的操作,效率高
資料結構 陣列+連結串列+紅黑樹(連結串列長度超過8則轉紅黑樹) 陣列+連結串列 陣列+連結串列+紅黑樹(連結串列長度超過8則轉紅黑樹)
是否允許null鍵值
雜湊地址演算法 (key的hashCode)^(key的hashCode無符號右移16位) key的hashCode ( (key的hashCode)^(key的hashCode無符號右移16位) )&0x7fffffff
定位演算法 雜湊地址&(容量大小-1) (雜湊地址&0x7fffffff)%容量大小 雜湊地址&(容量大小-1)
擴容演算法 當鍵值對數量大於閾值,則容量擴容到原來的2倍 當鍵值對數量大於等於閾值,則容量擴容到原來的2倍+1 當鍵值對數量大於等於sizeCtl,單執行緒建立新雜湊表,多執行緒複製bucket到新雜湊表,容量擴容到原來的2倍
連結串列插入 將新節點插入到連結串列尾部 將新節點插入到連結串列頭部 將新節點插入到連結串列尾部
繼承的類 繼承abstractMap抽象類 繼承Dictionary抽象類 繼承abstractMap抽象類
實現的介面 實現Map介面 實現Map介面 實現ConcurrentMap介面
預設容量大小 16 11 16
預設負載因子 0.75 0.75 0.75
統計size方式 直接返回成員變數size 直接返回成員變數count 遍歷CounterCell陣列的值進行累加,最後加上baseCount的值即為size

參考

【死磕Java併發】—–J.U.C之Java併發容器:ConcurrentHashMap
Map 大家族的那點事兒 ( 7 ) :ConcurrentHashMap
Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
Java 8 ConcurrentHashMap原始碼分析

原文地址:https://ddnd.cn/2019/03/10/jdk1-8-concurrenthashmap/

相關文章