ConcurrentHashMap原始碼分析-JDK18

LemonDus發表於2024-12-05

前言

ConcurrentHashMap是一個執行緒安全的HashMap,主要用於解決HashMap中併發問題。

在ConcurrentHashMap之前,也有執行緒安全的HashMap,比如HashTableCollections.synchronizedMap,但普遍效率低下。

Hashtable效率不高是因為它對資料操作的時候都會透過synchronized上鎖,也就是我們在講synchronized說的同步方法。而Collections.synchronizedMap的效率不高是因為在SynchronizedMap內部維護了一個普通物件Map,還有 排斥鎖mutex,我們在呼叫這個方法的時候就需要傳入一個Map,mutex引數可以傳也可以不傳。建立出synchronizedMap之後,再操作map的時候,就會對這些方法上鎖(如下),也就是我們說的同步程式碼塊,所以效能不高。
synchronizedMap

因此,才有了JDK1.5引入的ConcurrentHashMap!

ConcurrentHashMap在JDK1.7之前變化不大,在1.8中修改了較多,下面分析一下1.7和1.8中的變化:

  • 鎖方面: 由分段鎖(Segment繼承自ReentrantLock)升級為 CAS+synchronized實現;
  • 資料結構層面: 將Segment變為了Node,減小了鎖粒度,使每個Node獨立,由原來預設的併發度16變成了每個Node都獨立,提高了併發度;
  • hash衝突: 1.7中發生hash衝突採用連結串列儲存,1.8中先使用連結串列儲存,後面滿足條件後會轉換為紅黑樹來最佳化查詢。由於使用了紅黑樹,jdk1.7中連結串列查詢複雜度為O(N),jdk1.8中紅黑樹最佳化為O(logN))
  • concurr_compare

繼承與實現

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable

可以看到ConcurrentHashMap繼承了AbstractMap,實現了ConcurrentMapSerializable
concurr_jg

AbstractMap,這是一個java.util包下的抽象類,提供Map介面的骨幹實現,以最大限度地減少實現Map這類資料結構時所需的工作量,一般來講,如果需要重複造輪子——自己來實現一個Map,那一般就是繼承AbstractMap。

ConcurrentHashMap實現了ConcurrentMap這個介面,ConcurrentMap是在JDK1.5時隨著J.U.C包引入的,這個介面其實就是提供了一些針對Map的原子操作:

package java.util.concurrent;

import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

public interface ConcurrentMap<K,V> extends Map<K,V> {

//返回指定key對應的值;如果Map不存在該key,則返回defaultValue
    default V getOrDefault(Object key, V defaultValue) { ...} 
    
//遍歷Map的所有Entry,並對其進行指定的aciton操作
    default void forEach(BiConsumer<? super K, ? super V> action) {...}
    
//如果Map不存在指定的key,則插入<K,V>;否則,直接返回該key對應的值
    V putIfAbsent(K key, V value);
    
//刪除與<key,value>完全匹配的Entry,並返回true;否則,返回false
    boolean remove(Object key, Object value);
    
//如果存在key,且值和oldValue一致,則更新為newValue,並返回true;否則,返回false
    boolean replace(K key, V oldValue, V newValue);
    
//如果存在key,則更新為value,返回舊value;否則,返回null
    V replace(K key, V value);
    
//遍歷Map的所有Entry,並對其進行指定的funtion操作
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {...}
    
//如果Map不存在指定的key,則透過mappingFunction計算出value並插入
    default V computeIfAbsent(K key , Function<? super K, ? extends V> mappingFunction{...}

//如果Map存在指定的key,則透過mappingFunction計算出value並替換舊值
    default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...}

//根據指定的key,查詢value;然後根據得到的value和remappingFunction重新計算出新值,並替換舊值
    default V compute(K key , BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...}

//如果key不存在,則插入value;否則,根據key對應的值和remappingFunction計算出新值,並替換舊值
    default V merge(K key, V value , BiFunction<? super V, ? super V, ? extends V> remappingFunction) {...}

Serializable則標誌這個類可以進行序列化。

資料結構

ConcurrentHashMap的資料結構對比HashMap要複雜很多,所以在看構造器之前先分析一下它的資料結構。
concurr_jg2

從上圖可以看出,table一共包含 4 種不同型別的桶,不同的桶用不同顏色表示,分別是NodeTreeBinForwardingNodeReservationNode這四種結點。

另外,TreeBin結點所連線的是一顆紅黑樹,紅黑樹結點使用TreeNode表示,加上前面四種一共是5種結點。

而這裡沒有直接使用TreeNode的原因是因為紅黑樹的操作比較複雜,包括構建、左旋、右旋、刪除,平衡等操作,用一個代理結TreeBin來包含這些複雜操作,其實是一種 “職責分離”的思想,另外TreeBin中也包含了一些加/解鎖操作。

Node結點

  • Node是其它四種型別結點的父類;
  • 預設連結到table[i],即桶上的結點就是Node結點;
  • 當出現hash衝突時,Node結點會首先以連結串列的形式連結到table上,當超過閾值的時候才會轉化為紅黑樹。
/**
 * 普通的Entry結點, 以連結串列形式儲存時才會使用, 儲存實際的資料.
 */
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;	//透過key計算hash值,透過hash值找相應的桶
    final K key;
    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;
    }

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

    /**
     * 連結串列查詢.
     */
    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結點

TreeNode是紅黑樹的結點,TreeNode不會直接連結到桶上面,而是由TreeBin連結,TreeBin會指向紅黑樹的根結點。

/**
 * 紅黑樹結點, 儲存實際的資料.
 */
static final class TreeNode<K, V> extends Node<K, V> {
    boolean red;

    TreeNode<K, V> parent;
    TreeNode<K, V> left;
    TreeNode<K, V> right;

    /**
     * prev指標是為了方便刪除.
     * 刪除連結串列的非頭結點時,需要知道它的前驅結點才能刪除,所以直接提供一個prev指標
     */
    TreeNode<K, V> prev;

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

    Node<K, V> find(int h, Object k) {
        return findTreeNode(h, k, null);
    }

    /**
     * 以當前結點(this)為根結點,開始遍歷查詢指定key.
     */
    final TreeNode<K, V> findTreeNode(int h, Object k, Class<?> kc) {
        if (k != null) {
            TreeNode<K, V> p = this;
            do {
                int ph, dir;
                K pk;
                TreeNode<K, V> q;
                TreeNode<K, V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                    (kc = comparableClassFor(k)) != null) &&
                    (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
        }
        return null;
    }
}

TreeBin結點

TreeBin相當於TreeNode的代理結點;TreeBin會直接連結到 table[i] 上,該結點提供了一系列紅黑樹相關的操作,以及加鎖、解鎖操作。

/*
 TreeNode的代理結點(相當於封裝了TreeNode的容器,提供針對紅黑樹的轉換操作和鎖控制)
 hash值固定為-2
*/
    static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;					// 紅黑樹結構的根結點
        volatile TreeNode<K,V> first;		// 連結串列結構的頭結點
        volatile Thread waiter;				// 最近的一個設定WAITER標識位的執行緒
        volatile int lockState;				// 整體的鎖狀態標識位,0為初始態
        // values for lockState
        static final int WRITER = 1; // 二進位制001,紅黑樹的寫鎖狀態
        static final int WAITER = 2; // 二進位制010,紅黑樹的等待獲取寫鎖狀態(優先鎖,當有鎖等待,讀就不能增加了)
	    // 二進位制100,紅黑樹的讀鎖狀態,讀可以併發,每多一個讀執行緒,lockState都加上一個READER值,
        static final int READER = 4; 
	   /*
  		 在hashCode相等並且不是Comparable型別時,用此方法判斷大小.
  	   */
        static int tieBreakOrder(Object a, Object b) {
            int d;
            if (a == null || b == null ||
                (d = a.getClass().getName().
                 compareTo(b.getClass().getName())) == 0)
                d = (System.identityHashCode(a) <= System.identityHashCode(b) ?  -1 : 1);
            return d;
        }

       // 將以b為頭結點的連結串列轉換為紅黑樹
        TreeBin(TreeNode<K,V> b) {...}
	   // 透過lockState,對紅黑樹的根結點➕寫鎖.
        private final void lockRoot() {
            if (!U.compareAndSetInt(this, LOCKSTATE, 0, WRITER))
                contendedLock(); // offload to separate method ,Possibly blocks awaiting root lock.
        }

		//釋放寫鎖
        private final void unlockRoot() { lockState = 0; }

	//  從根結點開始遍歷查詢,找到“相等”的結點就返回它,沒找到就返回null,當存在寫鎖時,以連結串列方式進行查詢,後會面會介紹
        final Node<K,V> find(int h, Object k) {... }

         /**
         * 查詢指定key對應的結點,如果未找到,則直接插入.
         * @return  直接插入成功返回null, 替換返回找到的結點的oldVal
         */
        final TreeNode<K,V> putTreeVal(int h, K k, V v) {...} 
  	    /*
   		   刪除紅黑樹的結點:
 		    1. 紅黑樹規模太小時,返回true,然後進行 樹 -> 連結串列 的轉化,最後刪除;
  		    2. 紅黑樹規模足夠時,不用變換成連結串列,但刪除結點時需要加寫鎖;
   	   */
        final boolean removeTreeNode(TreeNode<K,V> p) {...}

		// 以下是紅黑樹的經典操作方法,改編自《演算法導論》
        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root , TreeNode<K,V> p) { ...}
        static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root , TreeNode<K,V> p) {...}
        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root , TreeNode<K,V> x) {...}
        static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { ... }
        static <K,V> boolean checkInvariants(TreeNode<K,V> t) {...} //遞迴檢查紅黑樹的正確性
        private static final long LOCKSTATE= U.objectFieldOffset(TreeBin.class, "lockState");
}

ForwardingNode結點

ForwardingNode結點僅僅在 擴容 時才會使用

/**
 * ForwardingNode是一種臨時結點,在擴容進行中才會出現,hash值固定為-1,且不儲存實際資料。
 * 如果舊table陣列的一個hash桶中全部的結點都遷移到了新table中,則在這個桶中放置一個ForwardingNode,即table[i]=ForwardingNode。
 * 讀操作碰到ForwardingNode時,將操作轉發到擴容後的新table陣列上去執行;寫操作碰見它時,則嘗試幫助擴容。
 */
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;
    }

    // 在新的陣列nextTable上進行查詢
    Node<K, V> find(int h, Object k) {
        // loop to avoid arbitrarily deep recursion on forwarding nodes
        outer:
        for (Node<K, V>[] tab = nextTable; ; ) {
            Node<K, V> e;
            int n;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (; ; ) {
                int eh;
                K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K, V>) e).nextTable;
                        continue outer;
                    } else
                        return e.find(h, k);
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

ReservationNode結點

保留結點,ConcurrentHashMap中的一些特殊方法會專門用到該類結點。

/**
 * 保留結點.
 * hash值固定為-3, 不儲存實際資料
 * 只在computeIfAbsent和compute這兩個函式式API中充當佔位符加鎖使用
 */
static final class ReservationNode<K, V> extends Node<K, V> {
    ReservationNode() {
        super(RESERVED, null, null, null);
    }

    Node<K, V> find(int h, Object k) {
        return null;
    }
}

構造器方法

ConcurrentHashMap提供了五個構造器,這五個構造器內部最多也只是計算了下table的初始容量大小,並沒有進行實際的建立table陣列的工作。

因為ConcurrentHashMap用了一種懶載入的模式,只有到首次插入鍵值對的時候,才會真正的去初始化table陣列。

空構造器

public ConcurrentHashMap() {
}

指定table初始容量的構造器

/**
 * 指定table初始容量的構造器.
 * tableSizeFor會返回大於入參(initialCapacity + (initialCapacity >>> 1) + 1)的最小2次冪值
 */
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();

    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));

    this.sizeCtl = cap;
}

根據已有的Map構造

/**
 * 根據已有的Map構造ConcurrentHashMap.
 */
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

指定table初始容量和負載因子的構造器

/**
 * 指定table初始容量和負載因子的構造器.
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

指定table初始容量、負載因子、併發級別的構造器

/**
 * 指定table初始容量、負載因子、併發級別的構造器.
 * <p>
 * 注意:concurrencyLevel只是為了相容JDK1.8以前的版本,並不是實際的併發級別,loadFactor也不是實際的負載因子
 * 這兩個都失去了原有的意義,僅僅對初始容量有一定的控制作用
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();

    if (initialCapacity < concurrencyLevel)
        initialCapacity = concurrencyLevel;

    long size = (long) (1.0 + (long) initialCapacity / loadFactor);
    int cap = (size >= (long) MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int) size);
    this.sizeCtl = cap;
}

常量/欄位

原始碼中常量如下:

/**
 * 最大容量.
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 預設初始容量
 */
private static final int DEFAULT_CAPACITY = 16;

/**
 * 最大陣列長度
 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 負載因子,為了相容JDK1.8以前的版本而保留。
 * JDK1.8中的ConcurrentHashMap的負載因子恆定為0.75
 */
private static final float LOAD_FACTOR = 0.75f;

/**
 * 連結串列轉樹的閾值,即連結結點數大於8時, 連結串列轉換為樹.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 樹轉連結串列的閾值,即樹結點樹小於6時,樹轉換為連結串列.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 在連結串列轉變成樹之前,還會有一次判斷:
 * 即只有鍵值對數量大於MIN_TREEIFY_CAPACITY,才會發生轉換。
 * 這是為了避免在Table建立初期,多個鍵值對恰好被放入了同一個連結串列中而導致不必要的轉化。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 在樹轉變成連結串列之前,還會有一次判斷:
 * 即只有鍵值對數量小於MIN_TRANSFER_STRIDE,才會發生轉換.
 */
private static final int MIN_TRANSFER_STRIDE = 16;

/**
 * 用於在擴容時生成唯一的隨機數.
 */
private static int RESIZE_STAMP_BITS = 16;

/**
 * 可同時進行擴容操作的最大執行緒數.
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
 * The bit shift for recording size stamp in sizeCtl.
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

static final int MOVED = -1;                // 標識ForwardingNode結點(在擴容時才會出現,不儲存實際資料)
static final int TREEBIN = -2;              // 標識紅黑樹的根結點
static final int RESERVED = -3;             // 標識ReservationNode結點()
static final int HASH_BITS = 0x7fffffff;    // usable bits of normal node hash

/**
 * CPU核心數,擴容時使用
 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

原始碼中欄位如下:

/**
 * Node陣列,標識整個Map,首次插入元素時建立,大小總是2的冪次.
 */
transient volatile Node<K, V>[] table;

/**
 * 擴容後的新Node陣列,只有在擴容時才非空.
 */
private transient volatile Node<K, V>[] nextTable;

/**
 * 控制table的初始化和擴容(重要⭐⭐⭐)
 * 0  : 初始預設值
 * -1 : 有執行緒正在進行table的初始化
 * >0 : table初始化時使用的容量,或初始化/擴容完成後的threshold
 * -(1 + nThreads) : 記錄正在執行擴容任務的執行緒數
 */
private transient volatile int sizeCtl;

/**
 * 擴容時需要用到的一個下標變數.
 */
private transient volatile int transferIndex;

/**
 * 計數基值,當沒有執行緒競爭時,計數將加到該變數上。類似於LongAdder的base變數
 */
private transient volatile long baseCount;

/**
 * 計數陣列,出現併發衝突時使用。類似於LongAdder的cells陣列
 */
private transient volatile CounterCell[] counterCells;

/**
 * 自旋標識位,用於CounterCell[]擴容時使用。類似於LongAdder的cellsBusy變數
 */
private transient volatile int cellsBusy;

// 檢視相關欄位
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;

put()方法

put方法是ConcurrentHashMap類的核心方法

/**
 * 插入鍵值對,<K,V>均不能為null.
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

這裡提一嘴,為什麼ConcurrentHashMap和Hashtable的key和value都不允許為null,而HashMap可以呢?

這是因為ConcurrentHashMap和Hashtable都是支援併發的,這樣會有一個問題,當你透過get(k)獲取對應的value時,如果獲取到的是null時,你無法判斷,它是put(k,v)的時候value就為null,還是這個key從來沒有做過對映。

而HashMap是非併發的,可以透過contains(key)來進行校驗。而支援併發的Map在呼叫m.contains(key)和m.get(key)時m可能已經不同了。

put方法內部呼叫了putVal這個私有方法:

/**
 * 實際的插入操作
 * @param onlyIfAbsent true:僅當key不存在時,才插入
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());  // 再次計算hash值

    /**
     * 使用連結串列儲存時,binCount記錄table[i]這個桶中所儲存的結點數;
     * 使用紅黑樹儲存時,binCount==2,保證put後更改計數值時能夠進行擴容檢查,同時不觸發紅黑樹化操作
     */
    int binCount = 0;

    for (Node<K, V>[] tab = table; ; ) {            // 自旋插入結點,直到成功
        Node<K, V> f;
        int n, i, fh;
        if (tab == null || (n = tab.length) == 0)                   // CASE1: 首次初始化table —— 懶載入
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    // CASE2: table[i]對應的桶為null
            // 注意下上面table[i]的索引i的計算方式:[ key的hash值 & (table.length-1) ]
            // 這也是table容量必須為2的冪次的原因,讀者可以自己看下當table.length為2的冪次時,(table.length-1)的二進位制形式的特點 —— 全是1
            // 配合這種索引計算方式可以實現key的均勻分佈,減少hash衝突
            if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) // 插入一個連結串列結點
                break;
        } else if ((fh = f.hash) == MOVED)                          // CASE3: 發現ForwardingNode結點,說明此時table正在擴容,則嘗試協助資料遷移
            tab = helpTransfer(tab, f);
        else {                                                      // CASE4: 出現hash衝突,也就是table[i]桶中已經有了結點
            V oldVal = null;
            synchronized (f) {              // 鎖住table[i]結點
                if (tabAt(tab, i) == f) {   // 再判斷一下table[i]是不是第一個結點, 防止其它執行緒的寫修改
                    if (fh >= 0) {          // CASE4.1: table[i]是連結串列結點
                        binCount = 1;
                        for (Node<K, V> e = f; ; ++binCount) {
                            K ek;
                            // 找到“相等”的結點,判斷是否需要更新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;
                            if ((e = e.next) == null) {     // “尾插法”插入新結點
                                pred.next = new Node<K, V>(hash, key,
                                    value, null);
                                break;
                            }
                        }
                    } else if (f instanceof TreeBin) {  // CASE4.2: table[i]是紅黑樹結點
                        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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);     // 連結串列 -> 紅黑樹 轉換
                if (oldVal != null)         // 表明本次put操作只是替換了舊值,不用更改計數值
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);             // 計數值加1
    return null;
}   

putVal這個方法邏輯大概如下:

  • 首先根據key計算hash值(n - 1) & hash
  • 然後透過hash值與table容量進行執行,計算得到key對映到table上的索引;
  • 最後加入結點,這裡要略微複雜一些。

putVal()中的四種情況

1.首次初始化table —— 懶載入
之前分析構造器的時候以及put()原始碼的註釋中都說了,ConcurrentHashMap在構造的時候並不會始化table陣列,首次初始化就在這裡透過 initTable() 完成。

在分析initTable()的原始碼前,我們需要考慮一個問題,如果多個執行緒同時呼叫initTable()初始化Node陣列怎麼辦?要如何去選擇哪個執行緒去初始化?

實際上,在初始化陣列時使用了樂觀鎖CAS操作來決定到底是哪個執行緒有資格進行初始化。volatile變數(sizeCtl):它是一個標記位,用來告訴其他執行緒這個坑位有沒有人在,其執行緒間的可見性由volatile保證,CAS操作保證了設定sizeCtl標記位的可見性,保證了只有一個執行緒能設定成功。

/**
 * 初始化table, 使用sizeCtl作為初始化容量.
 */
private final Node<K, V>[] initTable() {
    Node<K, V>[] tab;
    int sc;
    while ((tab = table) == null || tab.length == 0) {  //自旋直到初始化成功
        if ((sc = sizeCtl) < 0)         // sizeCtl<0 說明table已經正在初始化/擴容,此時會讓出CPU時間片
            Thread.yield();
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {  // 將sizeCtl更新成-1,表示正在初始化中,如果CAS操作成功了,代表本執行緒將負責初始化工作
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);     // n - (n >>> 2) = n - n/4 = 0.75n, 前面說了loadFactor已在JDK1.8廢棄
                }
            } finally {
                sizeCtl = sc;               // 設定threshold = 0.75 * table.length
            }
            break;
        }
    }
    return tab;
}

2.table[i]對應的桶為空: 直接CAS操作佔用桶table[i];

3.發現ForwardingNode結點,說明此時table正在擴容,則嘗試協助進行資料遷移。後面會對helpTransfer()呼叫的核心方法transfer()進行分析,我這裡簡單瞭解一下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)) {			//sizeCtl加一,表示多一個執行緒進來協助擴容
        transfer(tab, nextTab); 			//擴容,後面詳細講解
        break;
      }
    }
    return nextTab;
  }
  return table;
}

4.出現hash衝突,也就是table[i]桶中已經有了結點

  • 當兩個不同key對映到同一個 table[i] 桶中時,就會出現這種情況:
    • 當table[i]的結點型別為Node——連結串列結點時,就會將新結點以“尾插法”的形式插入連結串列的尾部;
    • 當table[i]的結點型別為TreeBin——紅黑樹代理結點時,就會將新結點透過紅黑樹的插入方式插入。

get()方法

/**
 * 根據key查詢對應的value值
 *
 * @return 查詢不到則返回null
 * @throws NullPointerException if the specified key is null
 */
public V get(Object key) {
    Node<K, V>[] tab;
    Node<K, V> e, p;
    int n, eh;
    K ek;
    int h = spread(key.hashCode());     // 重新計算key的hash值
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {       // table[i]就是待查詢的項,直接返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        } else if (eh < 0)              // hash值<0, 說明遇到特殊結點(非連結串列結點), 呼叫find方法查詢
            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值計算對映到table的哪個桶,table[i];
  • 其次:如果table[i]的key和待查詢key相同,那直接返回(這時候不用判斷是不是特殊節點);
  • 最後:如果table[i]對應的結點是特殊結點(hash值小於0),則透過 find() 查詢,如果不是特殊節點,則按連結串列查詢;

注意,假設現在需要get一個下標為3的結點,但此時桶table[3]的節點正在遷移,突然有一個執行緒進來呼叫get方法,正好key又雜湊到桶table[3],此時怎麼辦?

此時使用的查詢方法就不是get()了,而是find()

find()方法

  1. Node結點的查詢(hash>=0)
    當槽table[i]被普通Node結點佔用,說明是連結串列連結的形式,直接從連結串列頭開始查詢:
/**
 * 連結串列查詢.
 */
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;
}
  1. TreeBin結點的查詢 (hash=-2)
    TreeBin的查詢比較特殊,我們知道當桶table[i]被TreeBin結點佔用時,說明連結的是一棵紅黑樹,並且由於紅黑樹的插入、刪除等操作會涉及整個結構的調整,所以通常存在讀寫併發操作的時候,是需要加鎖的。
/**
 * 從根結點開始遍歷查詢,找到“相等”的結點就返回它,沒找到就返回null
 * 當存在寫鎖或等待獲取寫鎖時,以連結串列方式進行查詢
 * 也就是說,只有讀鎖時,才紅黑樹查詢
 */
final Node<K, V> find(int h, Object k) {
    if (k != null) {
        for (Node<K, V> e = first; e != null; ) {
            int s;
            K ek;
            /**
             * 兩種特殊情況下以連結串列的方式進行查詢:
             * 1. WRITER---》有執行緒正持有寫鎖,這樣做能夠不阻塞讀執行緒
             * 2. WAITER ---》有執行緒等待獲取寫鎖,不再繼續加讀鎖,相當於“寫優先”模式
             */
            if (((s = lockState) & (WAITER | WRITER)) != 0) { 
                if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                e = e.next;     // 連結串列形式查詢,找到立即返回
            }
            //這時候就是按紅黑樹找了,讀執行緒數量加1(讀讀),讀狀態進行累加, READER==4
            else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) {
                TreeNode<K, V> r, p;
                try {
                    p = ((r = root) == null ? null : r.findTreeNode(h, k, null));  //紅黑樹的根節點非空才能找
                } finally {
                    Thread w;
              // 如果去除當前讀執行緒狀態,LOCKSTATE依舊錶示為有寫執行緒w因為讀鎖而阻塞並有讀執行緒,則告訴寫執行緒,它可以嘗試獲取寫鎖了,就是條件2
                    if (U.getAndAddInt(this, LOCKSTATE, -READER) == (READER | WAITER) && (w = waiter) != null)
                        LockSupport.unpark(w);
                }
                return p;
            }
        }
    }
    return null;
}
  1. ForwardingNode結點的查詢 (hash=-1)
    ForwardingNode是一種臨時結點,在擴容進行中才會出現,所以查詢也在擴容的table ----》nextTable 上進行 (連結串列遍歷)。
/**
 * 在新的擴容table—-》nextTable上進行查詢
 */
Node<K, V> find(int h, Object k) {
    // loop to avoid arbitrarily deep recursion on forwarding nodes
    outer:
    for (Node<K, V>[] tab = nextTable; ; ) {
        Node<K, V> e;
        int n;
        if (k == null || tab == null || (n = tab.length) == 0 || (e = tabAt(tab, (n - 1) & h)) == null)
            return null;
        for (; ; ) {
            int eh;
            K ek;
            if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
            if (eh < 0) {
                if (e instanceof ForwardingNode) { 
                    tab = ((ForwardingNode<K, V>) e).nextTable;
                    continue outer;
                } else
                    return e.find(h, k);  //連結串列遍歷
            }
            if ((e = e.next) == null)
                return null;
        }
    }
}
  1. ReservationNode結點的查詢
    ReservationNode是保留結點,不儲存實際資料,所以直接返回null
Node<K, V> find(int h, Object k) {
    return null;
}

計數方法

ConcurrentHashMap由於存在多執行緒的情況,所以其相關的計數方法也需要進行特殊處理。

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

CounterCell 這個槽物件,當出現併發衝突時,每個執行緒會根據自己的hash值找到對應的槽位置。

/**
 * 計數槽.
 * 類似於LongAdder中的Cell內部類
 */
static final class CounterCell {
    volatile long value;

    CounterCell(long x) {
        value = x;
    }
}

之前在putVal()方法中,新增新結點後會使用addCount()進行技術加1,原始碼如下:

/**
 * 更改計數值,並檢查長度是否達到閾值
 */
private final void addCount(long x, int check) {
    CounterCell[] as; //計數桶
    long b, s;
 // !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x):嘗試更新baseCount
 //1、如果counterCells不為null,則代表已經初始化了,直接進入if語句塊
 //2、若競爭不嚴重,counterCells有可能還未初始化,為null,先嚐試CAS操作遞增baseCount值
    if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { 
   //進入此語句塊有兩種可能:
    //1.counterCells被初始化完成了,不為null
    //2.CAS操作遞增baseCount值失敗了,說明出現併發衝突,則將計數值累加到Cell槽
        CounterCell a;
        long v;
        int m;
        boolean uncontended = true;    //標誌是否存在競爭
 //1.先判斷計數桶是否初始化,如果as=null,說明沒有,進入語句塊
 //2.判斷計數桶長度是否為空,若是進入語句塊
 //3.這裡做了一個執行緒變數隨機數,若桶的這個位置為空,進入語句塊(根據執行緒hash值計算槽索引)
 //4.到這裡說明桶已經初始化了,且隨機的這個位置不為空,嘗試CAS操作使桶加1,失敗進入語句塊
        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);       // 槽更新也失敗, 則會執行fullAddCount
            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();  //統計容器大小
        }
    }
}

當出現了併發衝突,則不會再用CAS方式來計數了,直接使用桶方式,從上面的addCount方法可以看出來,此時的countCell是為空的(或者不為空但CAS更新失敗),最終一定會進入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;
            ...
            //如果計數桶!=null,證明已經初始化,此時不走此語句塊
            if ((as = counterCells) != null && (n = as.length) > 0) {
              ...
            }
            //進入此語句塊進行計數桶的初始化
            //CAS設定cellsBusy=1,表示現在計數桶Busy中...
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                //若有執行緒同時初始化計數桶,由於CAS操作只有一個執行緒進入這裡
                boolean init = false;
                try {                           // Initialize table
                    //再次確認計數桶為空
                    if (counterCells == as) {
                        //初始化一個長度為2的計數桶
                        CounterCell[] rs = new CounterCell[2];
                        //h為一個隨機數,與上1則代表結果為0、1中隨機的一個
                        //也就是在0、1下標中隨便選一個計數桶,x=1,放入1的值代表增加1個容量
                        rs[h & 1] = new CounterCell(x);
                        //將初始化好的計數桶賦值給ConcurrentHashMap
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    //最後將busy標識設定為0,表示不busy了
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //若有執行緒同時來初始化計數桶,則沒有搶到busy資格的執行緒就先來CAS遞增baseCount
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

從上面原始碼中可以看出,在CAS操作遞增計數桶失敗了3次之後,會進行擴容計數桶操作,注意此時同時進行了兩次隨機定位計數桶來進行CAS遞增的,所以此時可以保證大機率是因為計數桶不夠用了,才會進行計數桶擴容。

計數總結

  • 1、利用CAS遞增baseCount值來感知是否存線上程競爭,若競爭不大直接CAS遞增baseCount值即可,效能與直接baseCount++差別不大;
  • 2、若存線上程競爭,則初始化計數桶,若此時初始化計數桶的過程中也存在競爭,多個執行緒同時初始化計數桶,則沒有搶到初始化資格的執行緒直接嘗試CAS遞增baseCount值的方式完成計數,最大化利用了執行緒的並行。此時使用計數桶計數,分而治之的方式來計數,此時兩個計數桶最大可提供兩個執行緒同時計數,同時使用CAS操作來感知執行緒競爭,若兩個桶情況下CAS操作還是頻繁失敗(失敗3次),則直接擴容計數桶,變為4個計數桶,支援最大同時4個執行緒併發計數,以此類推…同時使用位運算和隨機數的方式"負載均衡"一樣的將執行緒計數請求接近均勻的落在各個計數桶中。

擴容機制

透過前面相關介紹,我們知道,當往table[i]中插入結點時,如果連結串列的結點數目超過一定閾值(8),就會觸發連結串列 -> 紅黑樹的轉換,這樣提高了查詢效率。

if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);             // 連結串列 -> 紅黑樹 轉換

接下來我們分析這個 連結串列 -> 紅黑樹 的轉換操作,treeifyBin(tab, i)

    /*
    *  連結串列 -> 紅黑樹 轉換
    */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n;
        if (tab != null) { 	
// CASE 1: table的容量 < MIN_TREEIFY_CAPACITY時,直接進行table擴容,不進行紅黑樹轉換,MIN_TREEIFY_CAPACITY預設為64
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
// CASE 2: table的容量 ≥ MIN_TREEIFY_CAPACITY時,進行相應桶的連結串列 -> 紅黑樹的轉換
            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;
                        }
                    // 以TreeBin型別包裝,並連結到table[index]中
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

透過 treeifyBin(Node<K,V>[] tab, int index) 原始碼可以看出,連結串列 -> 紅黑樹這一轉換並不是一定會進行的:

  • 當桶的容量 < MIN_TREEIFY_CAPACITY(64),CurrentHashMap 會首先選擇擴容(呼叫 tryPresize() 把陣列長度擴大到原來的兩倍),而非立即轉成紅黑樹;
  • 當桶的容量 >= MIN_TREEIFY_CAPACITY(64),則選擇 連結串列 -> 紅黑樹。

再看一下 tryPresize() 如何執行擴容:

   /*
   * 嘗試對table陣列進行擴容
   * @param 待擴容的大小
   */
    private final void tryPresize(int size) {   //jdk16
 	   // 視情況將size調整為2的整數次冪,與0.5 * MAXIMUM_CAPACITY來比較 , tableSizeFor求二次冪
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
    //CASE 1: table還未初始化,則先進行初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c; //取最大值
                if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
    // CASE2: c <= sc說明已經被擴容過了;n >= MAXIMUM_CAPACITY說明table陣列已達到最大容量
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
 // CASE3: 進行table擴容
            else if (tab == table) {
                int rs = resizeStamp(n);   
                // 這個CAS操作可以保證,僅有一個執行緒會執行擴容
                if (U.compareAndSetInt(this , SIZECTL , sc , (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

擴容原理

透過tryPresize() 我們發現呼叫了transfer方法,該方法可以被多個執行緒同時呼叫,是“資料遷移”的核心操作方法, 接下來我們看一看

    /**
	 * 資料轉移和擴容.
	 * 每個呼叫tranfer的執行緒會對當前舊table中[transferIndex-stride, transferIndex-1]位置的結點進行遷移
	 *
	 * @param tab     舊table陣列
	 * @param nextTab 新table陣列
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride; 
   // stride可理解成“步長”,即“資料遷移”時,每個執行緒要負責舊table中的多少個桶,根據幾核的CPU決定“步長”,最少16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //MIN_TRANSFER_STRIDE預設16
            stride = MIN_TRANSFER_STRIDE; // subdivide range  
        if (nextTab == null) { // 第二個引數,nextable為null說明第一次擴容
            try {
                @SuppressWarnings("unchecked")
               // 建立新table陣列,擴大一倍,32,n還為16
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;   
            } catch (Throwable ex) {      // 處理記憶體溢位(OOME)的情況
                sizeCtl = Integer.MAX_VALUE;   //將表示容量的sizeCtl 設定為最大值,然後返回
                return;
            }
            nextTable = nextTab; //設定nextTable變數為擴容後的陣列
            transferIndex = n;  // [transferIndex-stride, transferIndex-1]:表示當前執行緒要進行資料遷移的桶區間
        }
        int nextn = nextTab.length;
   // ForwardingNode結點,當舊table的某個桶中的所有結點都遷移完後,用該結點佔據這個桶
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
	 // 標識一個桶的遷移工作是否完成,advance == true 表示可以進行下一個位置的遷移
        boolean advance = true;
    // 最後一個資料遷移的執行緒將該值置為true,並進行本輪擴容的收尾工作
        boolean finishing = false; // to ensure sweep before committing nextTab
 	// i標識桶索引, bound標識邊界
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
    // 每一次自旋前的預處理,主要是為了定位本輪處理的桶區間
    // 正常情況下,預處理完成後:i == transferIndex-1:右邊界;bound == transferIndex-stride:左邊界
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
 // CASE1:當前是處理最後一個tranfer任務的執行緒或出現擴容衝突    
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {  // 所有桶遷移均已完成
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                   // 擴容執行緒數減1,表示當前執行緒已完成自己的transfer任務
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                 // 判斷當前執行緒是否是本輪擴容中的最後一個執行緒,如果不是,則直接退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                      /**
     * 最後一個資料遷移執行緒要重新檢查一次舊table中的所有桶,看是否都被正確遷移到新table了:
       * ①正常情況下,重新檢查時,舊table的所有桶都應該是ForwardingNode;
       * ②特殊情況下,比如擴容衝突(多個執行緒申請到了同一個transfer任務),此時當前執行緒領取的任務會作廢,那麼最後檢查時,
       * 還要處理因為作廢而沒有被遷移的桶,把它們正確遷移到新table中
       */
                    i = n; // recheck before commit
                }
            }
// CASE2:舊桶本身為null,不用遷移,直接嘗試放一個ForwardingNode
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
// CASE3:該舊桶已經遷移完成,直接跳過
            else if ((fh = f.hash) == MOVED)	
                advance = true; // already processed
// CASE4:該舊桶未遷移完成,進行資料遷移
            else {								
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
     // CASE4.1:桶的hash>0,說明是連結串列遷移
                        if (fh >= 0) {			
           	        /**
                     * 下面的過程會將舊桶中的連結串列分成兩部分:ln鏈和hn鏈
                     * ln鏈會插入到新table的槽i中,hn鏈會插入到新table的槽i+n中
                     */
                            int runBit = fh & n;	// 由於n是2的冪次,所以runBit要麼是0,要麼高位是1
                            Node<K,V> lastRun = f; 	// lastRun指向最後一個相鄰runBit不同的結點
                            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;
                            }
                     // 以lastRun所指向的結點為分界,將連結串列拆成2個子連結串列ln、hn
                            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);		// ln連結串列存入新桶的索引i位置
                            setTabAt(nextTab, i + n, hn); 	// hn連結串列存入新桶的索引i+n位置
                            setTabAt(tab, i, fwd);			// 設定ForwardingNode佔位
                            advance = true;					// 表示當前舊桶的結點已遷移完畢
                        }
    // CASE4.2:紅黑樹遷移                      
                        else if (f instanceof TreeBin) {  
                        
                        /**
                         * 下面的過程會先以連結串列方式遍歷,複製所有結點,然後根據高低位組裝成兩個連結串列;
                         * 然後看下是否需要進行紅黑樹轉換,最後放到新table對應的桶中
                         */
                            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);				// 設定ForwardingNode佔位
                            advance = true;						// 表示當前舊桶的結點已遷移完畢
                        }
                        else if (f instanceof ReservationNode)  //jdk16特有,1.8沒有
                            throw new IllegalStateException("Recursive update");
                    }
                }
            }
        }
    }

至此,ConcurrentHashMap的大體原始碼就分析完畢啦!

相關文章