ConcurrentHashMap原始碼解析

神易風發表於2022-03-15
本篇出處ConcurrentHashMap原始碼解析,引入請註明出處

如果想更詳細瞭解ConcurrentHashMap 內部工作原來可以去看下我以前寫過一篇HashMap原始碼解析瞭解下hash 演算法原理、陣列擴容、紅黑樹轉換等等。

initTable

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) //這時已經有其他執行緒獲取到執行權,沉睡一會
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                try {
                    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 = sc;
                }
                break;
            }
        }
        return tab;
    }

這個是初始化map陣列,核心是sizeCtl 這個變數:一個使用volatile修飾共享變數,作用通過交換比較競爭獲取初始化或者擴容陣列執行權。當執行緒發現sizeCtl小於0的時候,執行緒就會讓出執行權,當執行緒成功競爭設定-1 就相當於獲取到執行權,所以就可以去初始化陣列了。當為正數時,儲存下一個要調整Map大小的元素計數值

說明下Unsafe 交換比較方法

    /**
     * 通過物件屬性的值,修改前、更新後一致,才是更新成功
     * @param o 需要被修改的屬性的物件
     * @param l 物件Field的指標地址,可以理解成屬性值引用
     * @param i 修改前的值
     * @param i1 修改後的值
     * @return
     */
    public final native boolean compareAndSwapInt(java.lang.Object o, long l, int i, int i1);

put方法解析

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


    final V putVal(K key, V value, boolean onlyIfAbsent) {
       //ConcurrentHashMap 不允許key value為空,因為在併發情況下不能通過獲取get(key) 判斷key存不存在
        if (key == null || value == null) throw new NullPointerException(); 
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //通過計算hash得到陣列下標,為空通過交換比較設定進去,這時不需要加鎖的
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)  //這種情況說明陣列正在擴容,需要對連結串列和黑紅樹進行遷移
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) { //頭節點沒有改變,說明獲取鎖過程,沒有執行緒擴容
                        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;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key, value);
                                    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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

簡單說下put 業務邏輯:

  1. 先判斷table陣列是否為空,不加鎖,呼叫initTable 進行初始化
  2. 計算key 雜湊值,通過hash計算出陣列中下標,如果下標剛好為空,通過交換比較設定進去。多個執行緒設定通過陣列下標時,當設定失敗時會不滿足下標為空情況,進入獲取頭節點鎖
  3. 此時節點有值,並且hash 值等於-1,則說明陣列正在擴容,呼叫helpTransfer 方法多執行緒進行copy陣列
  4. 只鎖住連結串列或紅黑樹根節點,先判斷根節點是否等於獲取鎖的物件,因為有可能獲取到鎖物件已經因為陣列擴容進行偏移了 ,如果已經進行偏移還在根節點進行插入會導致hash計算錯誤,導致get 獲取不到值。這個是安全檢查很重要的。
  5. 為什麼要判斷fh >= 0 主要是因為ConcurrentHashMap Node hash 有特殊意義

    • int MOVED = -1; 正在進行陣列擴容,準備遷移
    • int TREEBIN = -2 紅黑樹根節點
    • int RESERVED = -3; 臨時保留節點
  6. 大於0就知道這是一個連結串列,從頭開始遍歷到最後插入,Java8 連結串列改成尾插入有一個好處就是遍歷完連結串列後就知道連結串列長度了,binCount就是連結串列長度。每次遍歷節點都會比較key相等,覆蓋舊值,否則在連結串列最後插入。
  7. 紅黑樹邏輯插入和連結串列差不多,通過putTreeVal 遍歷並且插入節點,只要找到相同key節點才會返回節點物件,如果是在紅黑樹下新增只會返回null。
  8. binCount是連結串列長度,如果長度大於連結串列長度閾值(預設8)轉換成紅黑樹。因為上面遍歷時遇到相同key值,都會將節點賦值給oldVal,如果不為空則是覆蓋,不需要執行累加,直接返回就可以了。
  9. addCount只要就是對map size進行累加,因為在併發情況下不能直接加鎖住size方法進行加一,這違反設定的粗細粒鎖的設定。實際情況比較複製,陣列擴容的邏輯也是在這個方法中實現的,下面具體分析下。

size() 方法

在分析map size之前,看講下size方法如何獲取長度,有利於addCount 講解。

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }
    final long sumCount() {
        CounterCell[] cs = counterCells;
        long sum = baseCount;
        if (cs != null) {
            for (CounterCell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }

size長度主要是通過baseCount + CounterCell陣列累加起來的。baseCount 只要當一個執行緒在使用map時,才會使用baseCount來記錄size大小,當出現多個執行緒執行緒同時對map操作時,就會放棄使用baseCount,而將每個執行緒運算元量 放入CounterCell陣列中。可以理解CounterCell只是一個計算盒子,通過一些演算法將不同執行緒運算元量放入到指定index位置,可以隔離執行緒對同個數競爭修改,有點類似hash 計算下標放入陣列方式,這樣可以減少衝突次數。

addCount分析

    private final void addCount(long x, int check) {
        CounterCell[] cs; long b, s;
        if ((cs = counterCells) != null ||
            !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell c; long v; int m;
            boolean uncontended = true;
            // 此時cs 沒有初始化,或者 cs剛剛初始化,長度還是0,通過執行緒隨機數獲取下標剛好為空
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
                fullAddCount(x, uncontended);  //將累加值新增到陣列中
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) { //于于0不會判斷陣列擴容情況
            Node<K,V>[] tab, nt; int n, sc;
            // 只有滿足size長度等於或大於sizeCtl 擴容閾值長度,才會進行下面邏輯
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
                if (sc < 0) { //當前tab陣列來擴容,通過競爭去搶奪擴容權
                    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

先從if判斷開始,如果countCells 這個陣列不空,第二個條件不用判斷,進入countCells設定累加。第一個條件不成立就會執行 baseCount累加,累加成功條件不成立,不進入if邏輯。
countCells 初始化、x值新增到陣列上,或者多執行緒對同一個陣列下標累加的控制都需要fullAddCount去實現的。
上面說過sizeCtl 為正整數就是陣列容量擴容閾值,while 下先判斷map size 是否達到或者超過閾值,符合條件就會
進入迴圈了進行陣列擴容。當sizeCtl > 0 時則是多個執行緒在對陣列擴容,這時需要競爭獲取執行權。
具體分析下擴容條件如何成立的,首先直到

resizeStamp(n) << RESIZE_STAMP_SHIFT;

這個方法是將陣列n,低位補零左移16為,相當於講陣列長度儲存到高16位中。每一個執行緒參與併發就會在sizeCtl 上+1 ,低16位這樣就是用來儲存參與陣列擴容數量。

  • sc > 0

    • 這時還沒有執行緒對tab陣列進行擴容
  • sz < 0

    • 已經有執行緒在擴容,將sizeCtl+1並呼叫transfer()讓當前執行緒參與擴容。

下面分析下while 裡面的判斷

  • sc == rs + MAX_RESIZERS 當前執行緒數已經達到最大上限了,再有新的執行緒進來擴容就沒有意義了
  • sc == rs + 1 只要rs + 1成功會在呼叫transfer,但是現在rs值已經被其他執行緒修改了,累加已經失敗了,沒有必要在執行一次交換比較了。
  • (nt = nextTable) == null 此時擴容已經完成了,新的執行緒不必進入擴容方法了
  • transferIndex <= 0 transferIndex tab陣列沒有分配的遷移的數量,此時為0表示擴容執行緒已經達到最大數量了,不必再使用新的執行緒進入了

細講下fullAddCount 方法
ThreadLocalRandom.getProbe()可以簡單理解成每一個執行緒hash值,但是這個值不是固定的,在找不到陣列slot是,可以重新計算。

   private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {  //初始化
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                
        for (;;) {
            CounterCell[] cs; CounterCell c; int n; long v;
            if ((cs = counterCells) != null && (n = cs.length) > 0) { 
                if ((c = cs[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 &&
                            U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {  //競爭成功了可以對陣列修改
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // 這時CAS已經失敗了,繼續自旋等待擴容
                    wasUncontended = true;      // Continue after rehash
                else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))  //累加成功,退出自旋
                    break;
                //當陣列長度超過CPU個數了,這時就不應該擴容了,而是繼續自旋直到累加成功
                else if (counterCells != cs || n >= NCPU) 
                    collide = false;            // At max size or stale
                else if (!collide) //如果競爭繼續失敗了,不擴容的話會繼續自旋
                    collide = true;
                else if (cellsBusy == 0 &&
                        //走到這裡說明第一次競爭失敗了,有衝突直接對資料擴容
                         U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == cs) // Expand table unless stale
                            counterCells = Arrays.copyOf(cs, n << 1);
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
               //重新生成probe ,因為此時是CAS插入失敗。 更換其他的插槽試試
                h = ThreadLocalRandom.advanceProbe(h); 
            }
            else if (cellsBusy == 0 && counterCells == cs &&
                     U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {//初始化陣列
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == cs) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
             // 這時候counterCells 陣列沒有初始化,有沒有多個執行緒競爭,可以使用baseCount 進行累加
            else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

cellsBusy 這個屬性跟我們之前講過sizeCtl 功能很相識的,都是來控制陣列修改許可權的。當cellsBusy =0時,counterCells陣列可以被插入、擴容的 ,執行緒只要將cellsBusy設定成1就可以獲取修改許可權,改完後將cellsBusy變成0就可以了。
看過LongAdder原始碼的同學是不是覺得很熟悉啊,不能說很相識,只是說是一模一樣,就連變數名都是複製過來的。其實這個我願意講這個細節的原因,涉及了LongAdder設計思想,value值分離成一個陣列,當多執行緒訪問時,通過hash演算法對映到其中的一個數字進行計數
圖來自 https://cloud.tencent.com/developer/article/1497528

transfer

接著看下map在多執行緒下如何進行擴容的

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //這裡是計算一個執行緒最大要遷移陣列個數,相當於將陣列分拆成這麼多部分,每個執行緒最大可以處理
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        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; //一個執行緒負責陣列上元素是否已經全部遷徙完成
        //i 表示上一次分配剩餘數量  bound陣列現在剩下數量
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
               //這裡只是為了i-1,或者就是所有陣列元素已經全部遷移完成了
                if (--i >= bound || finishing) 
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {//陣列已經全部被標註完了
                    i = -1;
                    advance = false;
                }
                 //這裡給進來執行緒分配遷移數量,分配成功就會跳出wile迴圈到下面去執行copy
                else if (U.compareAndSetInt
                         (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.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                   //在addCount 時 sc +2  就會進入擴容,現在再減回去 如果不相等則表示現在還要執行緒在處理資料擴容,否則將finish改成true表示擴容已經結束 
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) 
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
           //下面進入陣列元素遷移,每處理完一個將advance 改成true 重新進入while 
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //表明這個節點已經有其他執行緒在處理了
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {  //要遷移這個元素,直接鎖住
                synchronized (f) {
                    if (tabAt(tab, i) == f) { //雙重檢查
                        Node<K,V> ln, hn; //將連結串列分拆成兩個 ln表示低位  hn高位
                        if (fh >= 0) { //上面說過,只要hash大於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;
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
            }
        }
    }

ForwardingNode 這個是一個特殊節點,在陣列遷移過程中,已經被遷移過去節點會被標註成這個物件。防止在遷移陣列過程中插入到舊陣列中。內部封裝一個find方法,可以去新陣列中查詢key value,下面get方法會用到的。
如何支援併發情況下,分配各個執行緒任務的。每一個執行緒進入都會在雙層迴圈的while中分配到任務。剛進來時前面兩個if、else if不會滿足條件,但是會講transferIndex重新賦值給nextIndex,此時nextIndex還是陣列長度來的,當開始競爭修改transferIndex 時,設定成功的執行緒,分配到了陣列下標 tab.leng -1 到 tab.length -1 - stride 這個範圍的下標,從陣列最後往前進行元素分配,並且將遷移完成index設定bound,i為陣列copy的下標,當下標已經遷移到下個執行緒開始位置時,就會跳出while迴圈了,否則每次進入while就為了讓下標 -1。競爭失敗的執行緒會重新進入whle迴圈,繼續去從transferIndex獲取分配數量。當transferIndex不為空時,表示陣列仍然有任務分配,繼續去競爭獲取。

if (i < 0 || i >= n || i + n >= nextn) { 當i < 0 成立時表示當前執行緒已經做完copy任務了, i >= n || i + n >= nextn都是跟下面i = n 相關,對當前陣列進行邊界檢查。我們上面說過sizeCtl低16位表達當前執行緒擴容數量,讓一個執行緒完成任務後,在sizeCtl -1 。sizeCtl -2 不相等主要和擴容時sizeCtl +2 呼叫transfer 向對應,不相等說明此時仍然有其他執行緒正在copy中,擴容沒有完成的,已經完成執行緒自動退出擴容方法。讓最後一個執行緒將完成收尾工作,將nextTab 變成tab。

get方法

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) { //陣列元素就是當前key
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0) //這裡是紅黑樹或者ForwardingNode  ,使用內部封裝方法去查詢
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

get方法沒有使用鎖,支援最大併發讀取,而且不會出現安全問題。當eh < 0 ,這時是紅黑樹的話,呼叫紅黑樹遍歷方法去操作節點。如果此時ForwardingNode,雖然這個節點的資料已經被遷移到新陣列去了,但是內部封裝find方法,去新的陣列中查詢。無論是哪種節點都可以找到對應key value。

刪除方法

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode()); //計算hash值
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)  //找不到對應key 節點,退出迴圈
                break;
            else if ((fh = f.hash) == MOVED)  //參考上面put 此時陣列在擴容,執行緒幫忙進入擴容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {  //鎖住陣列下標節點
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {  //此時是連結串列結構
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                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;
                            }
                        }
                        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));
                                }
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (validated) {
                    if (oldVal != null) {  //舊值不存在,才會真正去刪除節點資料
                        if (value == null)
                            addCount(-1L, -1);  //size -1
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

資料引用

https://xilidou.com/2018/11/27/LongAdder/

https://cloud.tencent.com/developer/article/1497528

https://blog.csdn.net/zzu_seu/article/details/106698150

相關文章