ConcurrentHashMap解析二(putVal方法的解析)

ConcurrentHashMap中put()這個方法很容易引起并發操作的問題,現在來研究下put()方法的實現

put方法

public V put(K key, V value) {
    return putVal(key, value, false);
}
  //onlyIfAbsent跟HashMap一樣,就是判斷是否要覆蓋,默認為false,覆蓋
final V putVal(K key, V value, boolean onlyIfAbsent) {
  //這句話可以看出,ConcurrentHashMap中不允許存在空值,這個是跟HashMap的區別之一
  //通過這個機制,我們可以通過get方法獲取一個key,如果拋出異常,說明這個key不存在
    if (key == null || value == null) throw new NullPointerException();
    //ConcurrentHashMap中的hash值計算方法,跟HashMap中的差不多,不過最后的結果要
     //與0x7fffffff進行與操作,這個地方我還不明白為什么有這個與操作
    int hash = spread(key.hashCode());
    //binCount=0說明首節點插入,未進行鏈表或紅黑樹操作,因為后面會對這個值進行更改
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
    //如果數組為空或者長度為0,進行初始化工作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
    //如果獲取位置的節點為空,說明是首節點插入情況
        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
        }
      //如果hash值等于MOVEN(默認-1),說明是協助擴容,這個在后邊講解ForwardingNode類
      //進行進一步解析
        else if ((fh = f.hash) == MOVED)
        //協助擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
          //對桶的首節點進行加鎖
            synchronized (f) {
                //雙重判定,為了防止在當前線程進來之前,i地址所對應對象已經更改
                if (tabAt(tab, i) == f) {
                //為什么fh一定要大于等于0,這個原因在于TreeBin的hash值的設定,TreeBin類型
                //的hash值默認設置為了-2
                    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;
                            //當到桶的結尾還沒找到,則新增一個Node
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //如果hash小于0,判斷是否是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;
                        }
                    }
                }
            }
        //如果binCount值不等于0,說明進行了鏈表或者紅黑樹操作
            if (binCount != 0) {
                //如果binCount大于8則進行樹化,但真正的轉換成紅黑樹不是8的長度
                //當長度超過64才會真正的樹化,處于8-64之間的還只是數組擴容
                if (binCount >= TREEIFY_THRESHOLD)
                    //這個方法我在紅黑樹一節詳細講解了,不清楚的可以看紅黑樹那節
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
      //計數方法
    addCount(1L, binCount);
    return null;
}

我對這個方法進行了注釋,可以直觀的看代碼進行了解,主要分析一下三個方法
1、數組初始化方法initTable()
2、線程協助擴容方法helpTransfer()
3、計數方法addCount()

數組初始化initTable()

我們來看下這個方法的代碼:

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
          //判斷是否數組為空或者數組長度為0(未初始化)
        while ((tab = table) == null || tab.length == 0) {
              //如果sizeCtl值小于0,則每個進入到這里的線程要作線程讓步操作
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //SIZECTL是地址偏移量,和前邊我們講到的tabAt()那三個方法的地址偏移量一樣
            //如果SIZECTL對應地址的值與sc相等,說明當前的線程是第一個到達這條語句的
             //線程,那么就會將SIZECTL地址所對應的值替換成-1,而SIZECTL地址偏移量對應的  
           // 對象就是sizeCtl。
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                      //再次進行判斷,防止在進行U.compareAndSwapInt(this, SIZECTL, sc, -1)的時候
                      //有其他線程并發進入方法,導致出錯,使用雙重鎖
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //如果未初始化,會將sizeCtl設置成為當前容量類似hashmap中閾值threshold
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

這個地方有點疑問,sizeCtl到底指代什么,是閾值還是容量,在傳入容量的構造方法中,是直接將cap賦值給sizeCtl,也就是說在這里的sizeCtl是容量大小,而數組初始化的時候,又將容量的0.75倍賦值給了sizeCtl,找了下網上的資料
引用這個鏈接(http://www.lxweimin.com/p/c0642afe03e0)的說法:
sizeCtl :默認為0,用來控制table的初始化和擴容操作,具體應用在后續會體現出來。
**-1 **代表table正在初始化
**-N **表示有N-1個線程正在進行擴容操作
其余情況:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默認是table大小的0.75倍,居然用這個公式算0.75(n - (n >>> 2))。
也就是說,sizeCtl的值有這幾種情況,跟hashmap中單一指代閾值和容量的不同

線程協助擴容方法helpTransfer()

講這個方法之前,我們需要先了解一些其他的類與方法,首先是ForwardingNode,一個特殊的Node節點,hash值為-1,其中存儲nextTable的引用。

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            //MOVEN默認值-1
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k){...}
    }

然后是方法resizeStamp(),這個方法是計算校驗碼的

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

好,現在我們來看下helpTransfer()方法的使用:

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        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;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }   

首先判斷是否能夠協助擴容的條件
1、數組不為空
2、傳入的首節點是ForwardingNode的實例
3、該實例的nextTable類型不為空
在這三個條件的前提下,進行下一步,通過resizeStamp()方法拿到一個校驗碼,然后再判斷sizeCtl的值是否小于0,因為sizeCtl負數表示正在擴容。

停止擴容的四個條件(這部分我還沒理解,等后面理解了回來補充):
1、(sc >>> RESIZE_STAMP_SHIFT) != rs
2、sc == rs + 1
3、 sc == rs + MAX_RESIZERS
4、transferIndex <= 0

如果符合擴容條件,就使用U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)在sc上加一,然后調用transfer()方法進行數據遷移,這個方法比較復雜,到時候拿出來單獨講.
參考鏈接:
https://blog.csdn.net/u011392897/article/details/60479937

計數方法addCount()

    private final void addCount(long x, int check) {
            CounterCell[] as; long b, s;
            //計數部分
            if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                CounterCell a; long v; int m;
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                      U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                    fullAddCount(x, uncontended);
                    return;
                }
                if (check <= 1)
                    return;
                s = sumCount();
            }
          //擴容部分
            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();
                }
            }
        }       

這個方法分為兩部分,第一部分是計數,第二部分是擴容
第一部分計數,
當counterCells為空并且U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)返回true,則直接到下一部分是否擴容的判斷,否則 CAS 失敗后轉入更新 counterCells ,防止CAS 自旋,使用的方法是fullAndCount(),這個方法取自striped64的LongerAdd的方法。
關于striped64的解析詳情可以看以下鏈接:
http://brokendreams.iteye.com/blog/2259857
說到這個,我就不得不提一下這個類CounterCell,這個類代碼如下:

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

我們看到這個類用@sun.misc.Contended進行了注釋,這個注釋是自動填充緩存行用的,為什么要自動填充緩存行?
因為這樣可以防止偽共享的情況,那么什么是偽共享呢?
我解釋一下,就是當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享,也就是避免頭結點和為節點加載到同一個緩存行,使頭尾節點在修改時不會互相鎖定,基于此,我們需要自動填充緩存行。
這個類中我們可以看到只有一個volatile修飾符的變量value,也就是單個counterCell對象累加值

第二部分擴容,
滿足當前容量大于sizeCtl值并且數組不為空,數組長度小于最大容量值的時候,就開始擴容,跟helpTransfer()方法中一樣,這里也進行了同樣的判定,當線程剛進來的時候,sc是正的,所以執行else if的語句,U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)
這條語句將sizeCtl直接賦值成了一個負數,如果賦值成功,則調用數據遷移方法transfer().
在代碼的最后有個s = sumCount()的語句,這個是ConcurrentHashmap內部計數完反饋給我們的值,在size()中也有相關調用:

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

我們來看下sumCount()方法的內部實現:

    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;
    }

可以看出這個計算是分為兩部分,基礎部分是baseCount部分,然后還要加上countCells數組的所有值,通過之前的分析我們知道,baseCount是CAS成功后會自動累加的值,而countCells數組是在CAS失敗,也就是出現并發的情況下,進行累加的數組,這個數組類似segmenet一樣,采用分段鎖,最后將二者的值相加從而得到一個近似值。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉載自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay閱讀 6,174評論 1 5
  • 1.ConcurrentHashmap簡介 在使用HashMap時在多線程情況下擴容會出現CPU接近100%的情況...
    huanfuan閱讀 613評論 0 2
  • 昨天因為孩子不收拾衛生的問題在群里求助,得到姐妹們的各種建議,真的很開心,能量得到了加持,在這個群里,我們可以敞開...
    李沁李沁閱讀 185評論 0 0
  • 張力佳站在凱麗絲大廈前,無力地掛斷電話,又一次被女朋友姚麗麗拒絕。 她說要加班,晚上不能一起吃飯。 他不相信地搖搖...
    沐玉聲聲閱讀 427評論 1 9
  • 塵歸塵,土歸土 大地是一本書 你在天上想起誰,翻開書,看看 你那雙善良的眼睛望著誰,打開書看看 那是最疼的唏噓 春...
    琴歌素簡閱讀 258評論 0 0