java8 ConcurrentHashMap源碼學習

ConcurrentHashMap

ConcurrentHashMap是一個HashMap的升級版,是線程安全的,想要了解ConcurrentHashMap就必須得要去了解他的put、get、擴容方法
這裏必須說一下的是, 1.8的ConcurrentHashMap不是使用segment進行併發操作了, 現在太多誤導人的博客了… 雖然源碼中有segment但是整個put, get, 擴容的環節都與segment無關, 並沒有使用segment進行併發控制

put

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

可以看見這裏調用了putVal

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        // 這裏binCount是用於標記當前鏈表的長度
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // tab如果是空或者長度爲0就進行初始化
            // 在initTable中用cas替換sizeCtl爲-1保證了table在多線程下只會被一個線程初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // tabAt方法中使用了本地方法getObjectVolatile,直接從內存中獲取數組指定位置的值
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	// 該位置值爲空則new一個結點使用cas替換, 這裏cas保證了多線程下的原子性
            	// 若多個線程進入這一語句塊, cas先比較table[i]的值是否爲null, 若是則替換
            	// 所以只有一個線程的cas操作可以成功, 其他都失敗
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // MOVED代表着當前位置有線程在進行擴容遷移, 該線程會加入遷移過程
            // helpTransfer與transer操作幾乎一樣不多贅述
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // table已經被初始化過了, 當前節點不爲空, 並且擴容沒有在進行
            else {
                V oldVal = null;
                // 將f上鎖
                synchronized (f) {
                	// 再次進行判斷保證i結點還是之前的i結點
                    if (tabAt(tab, i) == f) {
                    	// fh>=0說明這個f是鏈表, 若f是treebin, 在treebin中是沒有hash這個變量的
                        if (fh >= 0) {
                            binCount = 1;
                            // 遍歷f, 看看key是否已經存在
                            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;
                                // key不存在, 新增一個結點
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 當前結點是紅黑樹, 直接調用putTreeVal
                        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, 調用treeifyBin
                	// 在treeifyBin中如果table容量小於64, 則會進行擴容而不是轉換爲紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 調用addCount, 將baseCount+1或者放入counterCells中
        addCount(1L, binCount);
        return null;
    }

addCount

這個函數的功能就是將baseCount+x或者將x放入counterCells

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // 首先判斷計數器是否爲空, 或者讓s=b+x與baseCount進行cas交換, 若失敗則進入語句塊
        // 這裏失敗了就會放棄累加baseCount轉而將x存入計數盒子
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            // 這裏有三個判斷
            // 1.計數器如果爲空則直接調用fullAddCount進行計數器初始化, 上一個if中cas失敗的場景
            // 2.取一個隨機數同數組-1進行與運算, 就是取餘獲取下標, 爲空則調用fullAddCount
            // 3.到了這一步即代表當前下標計數器不爲空, 所以取當前值與x相加, cas交換
            // 如果第三步也失敗了, 那就調用fullAddCount並且uncontended是false
            // fullAddCount就是一個計數的函數, 後面會講到
            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;
            }
            // 傳入的binCount <= 1 就直接return不進行擴容檢測
            if (check <= 1)
                return;
            // s是當前table中數據的總數, 這裏包括了baseCount和counterCells中的數
            // 由於這個sumCount()的實現非常簡單, 這裏就不贅述了
            s = sumCount();
        }
        // 傳入的binCount >= 0就進行擴容檢測, 這裏可以發現put過來的只要到了這一步一定會檢測
        // 而addCount還有被其他方法調用, 所以這裏需要做一個check判斷
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 這裏的s就是上面的sumCount計算出來的數據
            // s要大於需要擴容的長度, 基本上是table.length * 0.75
            // 若容量已經比2^31還要大就無法擴容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                // RESIZE_STAMP_BITS默認爲16
                // 這裏的rs用於標記, 是根據n和RESIZE_STAMP_BITS生成的一個高十六位負數
                int rs = resizeStamp(n);
                // sc正常來說是大於0的, 小於0的情況就是有線程在進行擴容, 那麼這個線程就加入幫助
                if (sc < 0) {
                	// 這裏判斷擴容是否已經完成
                	// 1.sc右移16位查看標誌位是否相等
                	// 2.sc如果等於rs+1說明擴容任務完成
                	// 3.幫助擴容的線程達到了了最大值
                	// 4.擴容完成的另一種判斷 nextTable是空
                	// 5.transferIndex小於等於0也說明擴容任務完成
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 當前線程加入擴容任務, sc++
                    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();
            }
        }
    }

fullAddCount

這一段挺難的, 牽扯到了很多種情況

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;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            if ((as = counterCells) != null && (n = as.length) > 0) {
            	// 當前的計數盒子中這個下標爲空, 表示可以存放一個新的數
                if ((a = as[(n - 1) & h]) == null) {
                	// 判斷這個計數器是否被上鎖, 這裏用上鎖的概念, 其實並不是真的上鎖了
                	// 因爲cas操作修改了cellsBusy, 保證只有一個線程執行這一個語句塊
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            // 一個創建標誌
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                // 創建一個新的結點賦值給計數器, 最後finally將cellsBusy賦值爲0
                                // 這一段看上去很像ReentrantLock的上鎖解鎖過程
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            // 如果創建成功直接break;
                            if (created)
                                break;
                            // 當前計數器不爲空, 後面進入的線程獲取了鎖
                            // 但是這個數值已經被前一個線程縮修改了
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                // cas操作已經失敗過, 這個變量是addCount傳入的, 表示對計數器的累加操作失敗
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // 與addCount中的cas操作一樣, 給計數器累加值, 成功就跳出, 失敗就往下
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                // 如果有其他線程創建了新的counterCells或者counterCells的容量大於cpu核心數
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                // collide是擴容標誌, 如果不允許擴容就會一直在上一步停留, 到了這一步就會允許擴容
                // 然後在下一次循環中直接進入擴容步驟
                else if (!collide)
                    collide = true;
                // 擴容步驟, cas獲取鎖, 擴容完畢後, 將擴容標誌改爲false並重新取一個隨機數
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            // 由於if中判斷的counterCells是空, 所以需要初始化計數盒子, cas獲取鎖
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                // 初始化成功標誌
                boolean init = false;
                // 初始容量是2, 然後將x放入這個計數盒子
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 計數盒子爲空且cellsBusy是1就給baseCount進行cas加操作
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

transfer

這個方法是擴容的主要方法, 很長

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 這裏的stride用於給線程分配任務, 這裏有n個位置需要進行遷移
        // 即一個線程需要處理的是transferIndex - stride ~ transferIndex個位置
        // 這裏根據cpu核心數和n來制定, stride最小爲16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 初始化, 傳入的nextTab如果是空, 說明需要初始化, 新數組比舊數組的容量大一倍
        // 這裏的初始化由外圍調用的方法保證只被初始化一次
        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就是上面說的線程分配任務的標誌位
            // 任務分配是數組從後往前分配的
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 這裏的ForwardingNode的hash值就是前面提到的MOVED, 只要一個結點是ForwardingNode
        // 那麼其他線程處理到這個結點的時候可以直接跳過
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // advance是一個標誌位, 表示遷移能否進行
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        // 從後往前
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // advance爲true表示可以進行遷移
            // 這裏的i可以理解爲是指向transferIndex的, 而bound指向transferIndex - stride
            while (advance) {
                int nextIndex, nextBound;
                // 這裏就是判斷遷移工作是否已經被分配
                // 前面說過一個線程完成stride個位置, i如果等於bound說明已經完成了stride個任務
                if (--i >= bound || finishing)
                    advance = false;
                // 這裏是判斷所有的遷移工作是否分配完畢, 因爲transferIndex是從後往前的
                // 所以如果transferIndex<=0那麼就說明所有遷移任務都分配完了了
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 當前線程獲取這個stride的任務, 前面判斷當前的遷移工作並未被分配
                // 所以將transferIndex - stride, 告訴後面線程這個工作我承包了
                // 如果transferIndex <= stride就直接將transferIndex 變爲0
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            // 經過了while循環獲取了工作的線程可以開始遷移工作
            // 這裏三個判斷條件都是判斷這個i是否符合遷移條件
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // finishing爲true表示所有工作已經完成
                // 這裏進行賦值處理
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 這裏用cas對sc-1表示當前線程完成了自己的工作, addCount中有說到sc的作用
                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
                }
            }
            // 如果f結點是空, 那麼用cas替換將tab[i]標誌爲MOVED
            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) {
                    	// 接下來的操作跟HashMap中的resize操作極其相似
                    	// 都是將當前的鏈表一分爲二, 一部分放在當前位置i, 一部分放在i+n
                    	// 置於爲什麼可以這樣操作在最後會講
                        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);
                            }
                            // cas操作替換值
                            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;
                                }
                            }
                            // 到此爲止與鏈表的操作無異, 也是將結點一分爲二生成兩個鏈表
                            // 這裏多了個判斷, 如果鏈表長度<=6, 那麼新結點中存放的是鏈表
                            // 否則, 判斷另一個鏈表是否爲空, 如果是, 不需要重新構造樹
                            // 如果不是, 那麼就要重新構造一棵樹
                            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;
                        }
                    }
                }
            }
        }
    }

這裏講一下鏈表可以一分爲二
假設n是16將要擴容爲32
因爲16是10000, 存入table中是同1111進行與運算的, 而存入新的table中是同11111進行與運算的
新數組中i存放的數據與i+n存放的數據唯一不同在於第五位二進制數是不是1
如果是1那麼就是存放在i+n的 如果是0就是存放在i中的
所以爲了判斷第五位是不是1, 就可以同16進行與運算如果第五位是0, 那麼他們與運算得出的結果就是0
所以可以把這個結點放入lo中, 而結果不爲0的就可以存放到i+n中
這個結果跟利用hash值重新同32-1進行與運算得出的結果一致

後記

寫完這一篇學習記錄之後, 更加清楚了ConcurrentHashMap中這幾個方法的用處, 閱讀源碼真的提升很大, ConcurrentHashMap中的併發設計十分精妙
而這一篇學習記錄也僅僅是將put的流程講述了一遍

java8 ConcurrentHashMap源碼學習2

============4.20更新============
之前就一直有一個疑惑, 爲什麼table需要使用tabAt()這個方法去獲取
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
	public native Object getObjectVolatile(Object var1, long var2);

通過以上代碼可以看見, tabAt就是使用Unsafe類中的本地方法, 直接從內存中獲取這個對象, 使用的是tab的內存地址+索引的偏移量去讀取, 可是table和Node中的next在ConcurrentHashMap中本身就是用volatile關鍵字修飾的, 直接讀取也是保證可見性的
所以這個問題無法從這個出發點去解決, 後來閱讀了ArrayList的源碼之後發現, 數組類在jvm中會自動檢測數組的越界問題

對於數組類型,每一維度將使用一個前置的“[”字符來描述,如一個定義爲“java.lang.String[][]”類型 的二維數組將被記錄成“[[Ljava/lang/String;”,一個整型數組“int[]”將被記錄成“[I”。

如果C是一個數組類型,並且數組的元素類型爲對象,也就是N的描述符會是類 似“[Ljava/lang/Integer”的形式,那將會按照第一點的規則加載數組元素類型。如果N的描述符如前面所 假設的形式,需要加載的元素類型就是“java.lang.Integer”,接着由虛擬機生成一個代表該數組維度和元 素的數組對象。

上面是深入理解java虛擬機一書中與數組類型有關的內容, 第二段是類加載的解析階段中所描述的, 所以Node<K, V>[]會先被包裝成一個數組類, 所以如果在數組中獲取下標爲-1的元素會直接拋出異常
這可能會帶來一些性能的消耗, 所以這裏使用Unsafe直接去操作內存讀取可能是出於性能方面的優化考量

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