【源碼閱讀】ConcurrentHashMap 1.8

一、爲什麼引入 ConcurrentHashMap 1.8 ?

  • JDK 1.7 採用分段鎖思想,整個 Hash 表被分成多個段,每個段中會對應一個 Segment 段鎖,段與段之間可以併發訪問,但是多線程想要操作同一個段是仍需要獲取鎖的。
  • JDK 1.8 在控制併發方面則取消了基於 Segment 的分段鎖思想,改用 CAS + synchronized 控制併發操作;在底層數據結構使用 Node 數組+鏈表+紅黑樹,但爲了兼容 jdk1.7,若仍保留了 segment 這個數據結構。

二、源碼閱讀

(1) 底層數據結構

成員變量定義了 ConcurrentHashMap 一些邊界值

	// node數組最大容量:2^30=1073741824
	private static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默認初始值,必須是2的幕數
	private static final int DEFAULT_CAPACITY = 16;
	//數組可能最大值,需要與toArray()相關方法關聯
	static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	//併發級別,遺留下來的,爲兼容以前的版本
	private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
	// 負載因子
	private static final float LOAD_FACTOR = 0.75f;
	// 鏈表轉紅黑樹閥值,> 8 鏈表轉換爲紅黑樹
	static final int TREEIFY_THRESHOLD = 8;
	//樹轉鏈表閥值,小於等於6(tranfer時,lc、hc=0兩個計數器分別++記錄原bin、新binTreeNode數量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))
	static final int UNTREEIFY_THRESHOLD = 6;
	static final int MIN_TREEIFY_CAPACITY = 64;
	private static final int MIN_TRANSFER_STRIDE = 16;
	private static int RESIZE_STAMP_BITS = 16;
	// 2^15-1,help resize的最大線程數
	private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
	// 32-16=16,sizeCtl中記錄size大小的偏移量
	private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
	// forwarding nodes的hash值
	static final int MOVED     = -1;
	// 樹根節點的hash值
	static final int TREEBIN   = -2;
	// ReservationNode的hash值
	static final int RESERVED  = -3;
	// 可用處理器數量
	static final int NCPU = Runtime.getRuntime().availableProcessors();
	//存放node的數組
	transient volatile Node<K,V>[] table;
	/*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
	 x = 0:默認值
	 x = -1:代表哈希表正在進行初始化
	 x < 0:相當於 HashMap 中的 threshold,表示閾值
	 x < -1:代表有多個線程正在進行擴容*/
	private transient volatile int sizeCtl;

再看一下底層數據結構 Node,它實現了 HashMap 中的 Entry,所以 Node 只不過是一個鍵值對。

static class Node<K,V> implements Map.Entry<K,V> {
    //鏈表的數據結構
    final int hash;
    final K key;
    //val和next都會在擴容時發生變化,所以加上volatile來保持可見性和禁止重排序
    volatile V val;
    volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    //不允許更新value 
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }
    //用於map中的 get() 方法,子類重寫
    Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }
}

紅黑樹節點 TreeNode 繼承了 Node

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }
}

TreeBin 用作樹的頭結點,只存儲 root 和 first 節點,不存儲節點的key、value值。

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;	     //紅黑樹的根節點
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
	//...
}

ForwardingNode 在數據轉移的時候放在頭部的節點,是一個空節點。

static final class ForwardingNode<K,V> extends Node<K,V> {
	final Node<K,V>[] nextTable;
	ForwardingNode(Node<K,V>[] tab) {
		super(MOVED, null, null, null);
		this.nextTable = tab;
	}
}

(2) 構造方法

public ConcurrentHashMap(int initialCapacity) {
	if (initialCapacity < 0)	 // 初始容量小於0,拋出異常
		throw new IllegalArgumentException();
	int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
			   MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 找到最接近該容量的2的冪次方數
	this.sizeCtl = cap;
}

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
	this.sizeCtl = DEFAULT_CAPACITY;
	putAll(m);	// 將集合m的元素全部放入
}

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();
	if (initialCapacity < concurrencyLevel)       // Use at least as many bins
		initialCapacity = concurrencyLevel;   // as estimated threads
	long size = (long)(1.0 + (long)initialCapacity / loadFactor);
	int cap = (size >= (long)MAXIMUM_CAPACITY) ?
		MAXIMUM_CAPACITY : tableSizeFor((int)size);
	this.sizeCtl = cap;
}

觀察最後一個構造方法發現新增了一個 sizeCtl 變量,它是怎麼得來的呢?

總結一下:

  • 該初始化過程通過指定的初始容量 initialCapacity,加載因子 loadFactor 和預估併發度concurrencyLevel 三個參數計算出一個最小的且大於等於 initialCapacity 大小的 2 的 n 次冪數,即 table 數組的初始大小 sizeCtl
  • 若 initialCapacity 爲 15,則 sizeCtl 爲 16
  • 但如果 initialCapacity 大小超過了允許的最大值,則 sizeCtl 爲最大值

(3) put 方法

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

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) 
	throw new NullPointerException();
    int hash = spread(key.hashCode());	// 得到 hash 值
    int binCount = 0;			// 用於記錄相應鏈表的長度
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果數組"空",進行 Node 數組初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 通過 hash 值算出對應的數組下標,從內存中得到第一個節點 f,並檢查是否爲空 ,如果是再用cas賦值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 用一次 CAS 操作嘗試將新值 f 放入位置 i
            // 如果 CAS 失敗,說明此時有併發操作使得該位置不爲空,那麼直接進入下一次循環 
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 走到這裏,f 就不爲空了;如果整個Map正在擴容
        else if ((fh = f.hash) == MOVED)
            // 幫助數據遷移,這個等到看完數據遷移部分的介紹後,再理解這個就很簡單了
            tab = helpTransfer(tab, f);
        else { // 到這裏就是說,f 是該位置的非空頭結點
            V oldVal = null;
            // 獲取數組該位置的頭結點的同步鎖
            synchronized (f) {
                if (tabAt(tab, i) == f) {//因爲在獲取鎖的過程中,可能被其他線程改變,所以再次檢查是否等於原值
                    if (fh >= 0) { 	// 頭結點的 hash 值大於 0,說明是鏈表
                        binCount = 1; 	// 記錄鏈表的長度
                        // 遍歷鏈表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果發生hash衝突且遇到"相等"的 key,則進行值覆蓋
                            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, null);
                                break;
                            }
                        }
                    } // 紅黑樹,這裏將 TreenNode 改爲 TreeBin 其實是爲了配合 Synchronize,原因如下:
		    //如果此處的結點爲紅黑樹,如果按照 HashMap 的方式去判斷並插入,會導致此處的頭結點會發生變化,而變化之後鎖住的對象就不是根節點了。
		    //而如果此處是一個 TreeBin,線程修改的只是 TreeBin 裏面的紅黑樹,無論裏面的樹怎麼改變,此處的鎖亦然不變。
		    //這種 synchronized (f) {...},寫法在 put 方法中也可見(f是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;
                        }
                    }
                }
            }//元素個數
            if (binCount != 0) {
                // 判斷鏈表長度是否 > 8
                if (binCount >= TREEIFY_THRESHOLD)
                    // 如果當前數組的總元素 < 64,那麼會選擇進行數組擴容,而不是轉換爲紅黑樹
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

put 方法的篇幅較長,總結一下:

  1. 根據 key 的 hashcode 值算出 hash 值,遍歷內部的 table,並判斷 tab 是否爲空:
    1. 如果 tab 爲空,則對 tab 進行初始化
    2. tab 不空則根據 hash 值得到 tab 的下標 i,拿到獲得該位置頭結點 f,判斷 f 是否爲空:
      • 如果 cas 檢查到 f 爲空,則將新對象放到位置 i,並結束 tab 的遍歷。
  2. 如果 f.hash == MOVED 則表示 此時數組正在擴容,則會去嘗試協助其他線程擴容。
  3. 如果 f 不空,則嘗試獲取結點 f 的同步鎖:
    1. 如果拿到鎖,就判斷 f 的類型:
      1. 如果是鏈表,檢查是否有 hash 衝突和 key 相同的情況,如果 onlyIfAbsent = false 則將舊值進行覆蓋;如果沒有衝突則使用尾插法將結點插入。
      2. 如果是紅黑樹,則執行紅黑樹的插入方法。
  4. 最後添加結點成功就調用 addCount 方法統計 size,並且在 addCount 方法中檢查是否需要擴容。
  • Q:那 put 方法是在哪些方面提供了線程安全保障的呢?如果怎麼保證線程的呢?
  • A:時機有:初始化數組時、添加結點是、以及擴容時。

3.1 initTable 方法

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // thread2在執行下面的compareAndSwapInt方法cas不成功,那麼thread2會到這個分支
        // 從而交出 CPU 等待下次系統調度
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 一定是隻有一個線程CAS成功,如果當前線程可以將 sizeCtl 設置爲 -1,代表搶到了機會
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {//有可能在這裏已經別其他線程初始化成功,所以再次判斷一下
                if ((tab = table) == null || tab.length == 0) {
                    // DEFAULT_CAPACITY 默認初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化數組,長度爲 16 或初始化時提供的長度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 將這個數組賦值給 table,table 是 volatile 的
                    table = tab = nt;
                    // 如果 n 爲 16 的話,那麼這裏 sc = 12;n - n/4 = 0.75 × n
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;// 設置 sizeCtl 爲 sc,我們就當是 12 吧
            }
            break;
        }
    }
    return tab;
}

初始化方法不難,也總結一下:

  1. 如果 tab 不空就直接返回。
  2. 如果 tab 爲空,每個併發線程就使用 cas 去競爭初始化機會:
    1. 如果某一個線程競爭不到機會,就會放棄競爭,自旋至 tab 不空爲止。
    2. 否則,就對 tab 進行初始化。

注: sizeCtl 默認爲 0,sizeCtl 中記錄 size 大小的偏移量,用來控制 table 的初始化和擴容操作.它的數值有以下含義:

  • -1:代表 table 正在初始化,其他線程應該交出 CPU 時間片,退出
  • -N: 表示正有 N-1 個線程執行擴容操作
  • >0:表示 tab 已經初始化,代表 tab 容量,默認爲 tab 大小的 0.75 倍,如果還未初始化,代表需要初始化的大小。

按照代碼的執行順序,如果沒有初始化,那麼可能就會進行擴容 transfer,下面就來聊一下擴容...


我們知道在 jdk1.7 中,最大併發量就是就是 seg 的個數,在存在併發操作時,雖然這樣設計使得 seg 對象在擴容時不會影響到其他 seg 對象,但如果該 seg 正在擴容,其他線程還得等到擴容完畢才能對 seg 對象進行讀寫,因此擴容效率就成爲了併發的一個瓶頸;

jdk1.8 就對這個問題進行了一個優化:首先 JDK1.8 去掉了分段鎖,將鎖的級別控制在了更細粒度的 Node 元素級別,同時作者 Doug lea 大神就認爲既然其他線程閒着也是閒着,不如一起參與擴容吧,於是在 jdk1.8 中就引入了一個 ForwardingNode 類以及一個 sizeCtl 來控制 table 的初始化和擴容操

3.2 helpTransfer 方法

我們在 put 方法中發現當 (fh = f.hash) == MOVED 成立時,會進入 helpTransfer 方法中。裏面涉及到了一個ForwardingNode類,先講講它是幹什麼的:

/**
 * A node inserted at head of bins during transfer operations.
 */
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
    ...
}

字面意思是:當整個 table 的某個位置正在進行擴容時,會把一個 ForwardingNode 類型的結點插在 table 的某個 tab[i] 的頭部,注意:並不是某個位置有 ForwardingNode 就表示擴容完全結束了,其他位置可能沒有。

作用不難想到

  • 一個用於連接兩個 table 的節點類。它包含一個 nextTable 指針,用於指向下一張表。而且這個節點的 key value next 指針全部爲n ull,它的 hash 值爲 -1
  • 在擴容時,線程 t1 會先判斷該槽點是否爲空:
    • 如果爲空,t1 就會將此處的第一個設置爲 forwordingNode,告訴其他線程此位置的數據遷移已有線程包辦。
    • 不爲空,則採用頭插法先把此位置的數據給遷移到新的數組中,最後給舊 table 的原位置賦值爲 fwd。

helptransfer參考:https://www.jianshu.com/p/39b747c99d32

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果 tab 不空並且結點 f 不是ForwardingNode類型,說明這個位置不在擴容(其他位置可能在擴容)。
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { 
        int rs = resizeStamp(tab.length);
        //若sizeCtl是負數,tab、nextTable和當前table、nextTable相同,說明擴容尚未完成
        //因爲一旦擴容完成,就會將cmap的屬性更新
        while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { 
            // 1.如果sizeCtl>>>16 != rs,則表示標識符變化了
            // 2.或者sizeCtl == rs + 1(擴容結束了,不再有線程進行擴容)(默認第一個線程設置sc == (rs左移16位+2),
                // 當第一個線程結束擴容,會將 sc 減一。此時sc=rs+1)
            // 3.或者sizeCtl == rs + 65535  (如果達到最大幫助線程的數量,即 65535)
            // 4.或者transferIndex正在調整 (擴容結束)
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0) 
                break;
            /* sc的含義
            -1:代表 table 正在初始化,其他線程應該交出 CPU 時間片,退出
            -N:表示正有 N-1 個線程執行擴容操作
            >0:表示 tab 已經初始化,代表 tab 容量,默認爲 tab 大小的 0.75,如果還未初始化,代表需要初始化的大小。*/
            // 用CAS嘗試將SIZECTL加1,表示表示增加了一個線程協助擴容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

總結一下 helpTransfer 方法:

  1. 先判斷當前位置的結點類型是否是 ForwardingNode,和當前結點的 nextTable 是否爲空:
  2. 如果 f 是 ForwardingNode 類型並且 f 的 nextTable 屬性爲空,則表示 map 不在擴容,返回一個 table 即可(可能是舊數組,也可能是新數組)
  3. 否則,進一步判斷 this.nextTable 是否和 f.nextTab 相等,以及 sc=sizeCtl 是否小於 0:
    • 如果 this.nextTable==f.nextTab,並且 sc < 0,則表示舊數組正在擴容,爲了保險,再次檢查 4 種代表擴容是否結束的情況是否符合:
      • 如果擴容結束,break 掉 while 返回即可
      • 否則,再次判段 sc 是否與內存中的 SIZECTL 相同:
        • 如果相同,則將用 cas 嘗試將 SIZECTL 加 1,表示表示增加了一個線程協助擴容,然後進入 transfer 方法。
        • 否則,回到 while 中再次檢查 this.nextTable==f.nextTab 是否成立,自旋。

存疑:

  • Q1:在總結的時候,我有一個疑惑,爲什麼一定要用 cas 檢查 SIZECTL,才能進入 transfer 方法呢?
    • A1:
  • Q2:爲什麼當 sc == rs + 1 就表示 cmap 擴容成功?
    • A2:這個判斷可以在 addCount 方法中找到答案:默認第一個線程設置 sc == (rs >>> 16) + 2,當第一個線程結束擴容了,就會將 sc--。此時 sc = rs + 1

如果上面的邏輯都沒問題,接下來就應該看看 cmap 是如何數據遷移的...


3.3 transfer 方法

數據遷移的邏輯比較複雜,因爲它支持併發擴容,而且還沒有加鎖。詳細看看 transfer 方法...

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // stride(步長)在單核下直接等於n,多核模式下爲 (n>>>3)/NCPU,最小值是16
    // 一共會有n個位置是需要進行遷移
    // n個位置分爲多個區域,每個區域有 stride 個任務
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 如果 nextTab 爲 null,先進行一次初始化
    // 保證只有第一個發起遷移的線程調用此方法時nextTab爲null
    if (nextTab == null) {
        try {// 容量翻倍
            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;//ConcurrentHashMap 中的屬性
        transferIndex = n;  //同上,用於控制遷移的位置,初始位置爲n
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance 指的是做完一個位置的遷移工作,可以準備做下一個位置的
    boolean advance = true;    //控制是否要繼續向前掃描
    boolean finishing = false; //確保在提交NextTab之前進行掃描,to ensure sweep before committing nextTab
    // i是當前掃描位置索引,bound是左邊界,注意是從後往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // advance爲true表示需要繼續向區域[bound, transferIndex]掃描
        // 簡單理解結局:i指向transferIndex,bound指向 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            // 如果--i>=bound證明當前掃描區域還沒有掃描完,所以沒必要擴張區域。第一次碰到不會執行(i初始爲0)
            if (--i >= bound || finishing) 
                advance = false;
            // 如果transferIndex<=0,說明舊數組的所有位置都有線程處理
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;    //退出循環
            }//如果cas成功則表示當前線程獲得了一個掃描區域
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
                        nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;  //nextBound 是這次遷移任務的邊界
                i = nextIndex - 1; 
                advance = false;   //當前線程已確定掃描區域
            }
        }//當前線程t進到這裏,表示t"沒事可幹"
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;             // sizeCtl的別名
            if (finishing) {    // 如果table的遷移操作已經完成
                nextTable = null;
                table = nextTab; // 將新的nextTab賦值給table,完成遷移
                // 重新計算 sizeCtl:n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75倍(4)
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 之前我們說過,sizeCtl 在遷移前會設置爲 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然後,每有一個線程參與遷移就會將 sizeCtl 加 1,
            // 這裏使用CAS操作對sizeCtl進行減1,代表做完了屬於自己的任務
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任務結束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 到這裏,說明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是說,所有的遷移任務都做完了,也就會進入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置i爲空,就用cas插入一個ForwardingNode結點
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 該位置處是一個ForwardingNode(MOVED的本質),代表該位置數據遷移完畢
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {// 獲取鎖並開始處理數組該位置處的遷移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 頭結點的hash值>0,表示此處爲鏈表
                    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);
                        }
                        // 低位鏈表放在新數組的位置i
                        setTabAt(nextTab, i, ln);
                        // 高位鏈表放在新數組的位置i+n
                        setTabAt(nextTab, i + n, hn);
                        // 將原數組該位置處設置爲fwd,標記該位置已經處理完畢,
                        setTabAt(tab, i, fwd);
                        // advance 設置爲 true,標記該位置已經遷移完畢
                        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;
                            }
                        }
                        // 如果低位鏈表的節點數少於 8,則將此處紅黑樹拆成鏈表
                        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;
                        // 將 ln 放置在新數組的位置 i
                        setTabAt(nextTab, i, ln);
                        // 將 hn 放置在新數組的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,
                        // 其他線程看到該位置的 hash 值爲 MOVED,則不進行遷移
                        setTabAt(tab, i, fwd);
                        // advance 設置爲 true,代表該位置已經遷移完畢
                        advance = true; //繼續向前掃描
                    }
                }
            }
        }
    }
}

整理一下思路:視頻 85:00

  1. ConcurrentHashMap 的擴容支持多線程,在多線程環境下,每一個線程會通過步長計算出自己的負責數據遷移的區域。
  2. 當線程在某個位置 i 遷移數據時,每一個線程都會鎖住 i 位置的頭結點,這樣保證了被鎖的位置不能被其他線程操作。
  3. 當位置 i 的數據被一個線程遷移後,該位置的鎖將會別釋放掉,並將此處的頭結點設置爲 ForwardingNode,告訴其他線程不必在繼續協助該位置,但沒有 ForwardingNode 的結點可以進行 put 操作,最後的數據還是會搬到新數組中, 與此同時,剛剛的線程繼續從右往左掃描,嘗試搬遷其他位置的數據:
    1. 如果有尚未搬遷而且未被其他線程正在搬遷的位置,該線程將會繼續變遷該位置的數據。
    2. 否則,該線程將會退出本次 transfer 的流程,繼續等待其他線程完成數據遷移。
  4. 所有數據遷移完全後,擴容完成。

3.3 treeifyBin 方法

如果要進入 treeifyBin 方法後判斷是否要進行樹化

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // MIN_TREEIFY_CAPACITY = 64
        // 如果數組長度小於 64 的時候,其實也就是 8 < x < 64 時會對數組擴容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 後面我們再詳細分析這個方法
            tryPresize(n << 1);
        // 如果 b 是非空頭結點
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    // 下面就是遍歷鏈表,建立一顆紅黑樹
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                        if ((p.prev = tl) == null) hd = p;
                        else			   tl.next = p;
                        tl = p;
                    }
                    // 將紅黑樹設置到數組相應位置中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

3.4 addCount 方法

addCount 方法不容易理解,簡單說一下它的作用吧:

  • 對 table 的總元素個數加一。無論是通過修改 baseCount,還是通過使用 CounterCell。當 CounterCell 被初始化了,就優先使用 CounterCell,不再使用 baseCount。
  • 檢查是否需要擴容,或者是否正在擴容。如果需要擴容,就調用擴容方法,如果正在擴容,就幫助其擴容。

對應視頻 JDK8中ConcurrentHashMap源碼解析(上) 的 65分鐘

(4) get 方法

get 方法相對比較簡單

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //計算hash值
    int h = spread(key.hashCode());
    //根據hash值確定節點位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //如果搜索到的節點key與傳入的key相同且不爲null,直接返回這個節點	
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果eh<0 說明這個節點在樹上 直接尋找
        else if (eh < 0)
            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;
}

簡單總結一下:

  • 計算 key 的 hash 值,然後再算出桶下標 i
  • 判斷 i 位置是否有元素:
    • 如果沒有元素,返回 null
    • 否則,如果 hash 值與 key 都相同,那麼返回當前位置元素的 val
  • 如果 hash 值與 key 都不相同,那麼在此檢查位置 i 的結點類型:
    • 如果是紅黑樹就調用紅黑樹的查找方法
    • 如果是鏈表就遍歷鏈表找出對應元素。

(5) size 方法




詳講每個方法,包括 UNsafe:https://blog.csdn.net/u010723709/article/details/48007881

addCount:https://www.jianshu.com/p/749d1b8db066 https://www.cnblogs.com/dgutfly/p/11425599.html


開源中國:https://my.oschina.net/hosee/blog/675884#h2_10 https://www.javadoop.com/post/hashmap#toc_11

https://blog.csdn.net/Bill_Xiang_/article/details/81122044

有目錄那個博客園:https://www.cnblogs.com/study-everyday/p/6430462.html#autoid-2-1-4

擴容時如何讀寫?https://www.baidu.com/link?url=d0a2BxAnZnDwfy4sDAs7fphVkYDWcIP_52yCGvpjL-1JS14N25ioMV6irOKTJfDXPqIITojxHNu5-oo6TdT7_oJ6C9Epw2uDavBj2VKhw3a&wd=&eqid=b18494ab000c3aad000000065ecb71f4

helptransfer、以及sizeCtl 參考:https://www.jianshu.com/p/39b747c99d32

代碼很多註釋:https://www.cnblogs.com/zerotomax/p/8687425.html#go2

講了finnish的怎麼得到:https://www.cnblogs.com/yangming1996/p/8031199.html

摺疊方法:https://www.cnblogs.com/leesf456/p/5453341.html

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