JDK原始碼閱讀(7):ConcurrentHashMap類閱讀筆記

pedro7發表於2021-11-25

ConcurrentHashMap

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

1. 一些重要引數

1.1 MAXIMUM_CAPACITY引數

/**
 * The largest possible table capacity.  This value must be
 * exactly 1<<30 to stay within Java array allocation and indexing
 * bounds for power of two table sizes, and is further required
 * because the top two bits of 32bit hash fields are used for
 * control purposes.
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY參數列示map的最大容量,預設為1 << 30。

1.2 DEFAULT_CAPACITY引數

/**
 * The default initial table capacity.  Must be a power of 2
 * (i.e., at least 1) and at most MAXIMUM_CAPACITY.
 */
private static final int DEFAULT_CAPACITY = 16;

DEFAULT_CAPACITY參數列示map的預設容量,為16。

1.3 MAX_ARRAY_SIZE引數

/**
 * The largest possible (non-power of two) array size.
 * Needed by toArray and related methods.
 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

MAX_ARRAY_SIZE參數列示map的陣列最大長度,在toArray()及其相關方法中可能用到。大小為Integer.MAX_VALUE - 8

1.4 DEFAULT_CONCURRENCY_LEVEL引數

/**
 * The default concurrency level for this table. Unused but
 * defined for compatibility with previous versions of this class.
 */
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

DEFAULT_CONCURRENCY_LEVEL參數列示預設併發級別。在筆者當前使用版本JDK13中已經被棄用,但為了和先前版本相容,保留這個引數。

1.5 LOAD_FACTOR引數

/**
 * The load factor for this table. Overrides of this value in
 * constructors affect only the initial table capacity.  The
 * actual floating point value isn't normally used -- it is
 * simpler to use expressions such as {@code n - (n >>> 2)} for
 * the associated resizing threshold.
 */
private static final float LOAD_FACTOR = 0.75f;

LOAD_FACTOR參數列示載入因子,預設和HashMap一樣,是0.75。

1.6 TREEIFY_THRESHOLD引數

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2, and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

TREEIFY_THRESHOLD參數列示陣列中連結串列轉換為紅黑樹的閾值,他被用於與一個連結串列的長度進行比較。

1.7 UNTREEIFY_THRESHOLD引數

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

UNTREEIFY_THRESHOLD參數列示陣列中紅黑樹轉變成連結串列的閾值,他被用於與一個紅黑樹的大小進行比較。

1.8 MIN_TREEIFY_CAPACITY引數

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * The value should be at least 4 * TREEIFY_THRESHOLD to avoid
 * conflicts between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

MIN_TREEIFY_CAPACITY參數列示將連結串列樹化的雜湊表最小容量。只有當整個ConcurrentHashMap的容量大於這個值時,具體的連結串列才可能發生樹化。如果沒有大於這個值,將進行擴容而非樹化。(擴容也會減少單個連結串列中的元素數量)。

1.9 MIN_TRANSFER_STRIDE引數

/**
 * Minimum number of rebinnings per transfer step. Ranges are
 * subdivided to allow multiple resizer threads.  This value
 * serves as a lower bound to avoid resizers encountering
 * excessive memory contention.  The value should be at least
 * DEFAULT_CAPACITY.STR
 */
private static final int MIN_TRANSFER_STRIDE = 16;

在擴容操作中,transfer這個步驟允許多執行緒併發進行,MIN_TRANSFER_STRIDE參數列示進行一次transfer操作中一個工作執行緒的最小任務量。即最少要處理的連續雜湊桶的數目,預設為16,即最少要對連續的16個雜湊桶進行transfer操作,詳見下文transfer()方法的解析。

1.10 RESIZE_STAMP_BITS引數(未理解)

/**
 * The number of bits used for generation stamp in sizeCtl.
 * Must be at least 6 for 32bit arrays.
 */
private static final int RESIZE_STAMP_BITS = 16;

RESIZE_STAMP_BITS引數用於生成在每次擴容中都唯一的生成戳。

1.11 MAX_RESIZERS引數(未理解)

/**
 * The maximum number of threads that can help resize.
 * Must fit in 32 - RESIZE_STAMP_BITS bits.
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

這個引數定義了resize時工作執行緒的最大數量,但這個計算方法我不明白。MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

1.12 RESIZE_STAMP_SHIFT引數(未理解)

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

這個引數定義了sizeCtl中記錄大小標記的位移位,但這個計算方法我不明白。MAX_RESIZERS = 32 - RESIZE_STAMP_BITS;

1.13 特殊節點的hash狀態引數

/*
 * Encodings for Node hash fields. See above for explanation.
 */
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations

正常情況下hash的值都應該是正數,如果是負數說明當前是一個不正常的、特殊的節點。

  • hash值為-1時,代表當前節點是一個Forwarding Node
    • ForwardingNode是一種臨時節點,在擴容進行中才會出現,並且它不儲存實際的資料。
    • 如果舊陣列的一個hash桶中全部的節點都遷移到新陣列中,舊陣列就在這個hash桶中放置一個ForwardingNode.
    • 讀操作或者迭代讀時碰到ForwardingNode時,將操作轉發到擴容後的新的table陣列上去執行,寫操作碰見它時,則嘗試幫助擴容。
  • hash值為-2時,代表當前節點是一個TreeBin
    • TreeBinConcurrentHashMap中用於代理操作TreeNode的特殊節點,持有儲存實際資料的紅黑樹的根節點。
    • 因為紅黑樹進行寫入操作,整個樹的結構可能會有很大的變化,這個對讀執行緒有很大的影響,所以TreeBin還要維護一個簡單讀寫鎖,這是相對HashMap,這個類新引入這種特殊節點的重要原因。
  • hash值為-3時,代表當前節點是一個保留節點,即佔位符。
    • 一般情況下不會出現。

1.14 HASH_BITS引數

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

HASH_BITSHashTable中也見到過,通過和它的位運算,可以將負數的雜湊值轉變為正數。

1.15 NCPU引數

/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();

NCPU引數可以獲取當前JVM能使用的處理器核心數。

2. 一些重要屬性

值得關注的是,ConcurrentHashMap中的關鍵屬性基本都是volatile變數。

2.1 table屬性

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 */
transient volatile Node<K,V>[] table;

table屬性用於存節點,是桶的集合。

2.2 nextTable屬性

/**
 * The next table to use; non-null only while resizing.
 */
private transient volatile Node<K,V>[] nextTable;

nextTable屬性表示下一個要使用的陣列,輔助resize操作,也僅在resize時非空。

2.3 baseCount屬性

/**
 * Base counter value, used mainly when there is no contention,
 * but also as a fallback during table initialization
 * races. Updated via CAS.
 */
private transient volatile long baseCount;

baseCount屬性是在沒有爭用現象時的基本計數器值,也在初始化表的競爭中使用。

2.4 sizeCtl屬性

/**
 * Table initialization and resizing control.  When negative, the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads).  Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.
 */
private transient volatile int sizeCtl;

sizeCtl屬性在表初始化和resize操作控制中發揮作用。

  • sizeCtl為負數,說明表正在進行初始化或resize操作。
    • 表初始化時為 -1。
    • resize時為-(1+擴容執行緒數)
  • sizeCtl為正數。
    • 表為null時為初始表大小或 0。
    • 表不為null時為需進行resize的下一個計數值。

2.5 transferIndex屬性

/**
 * The next table index (plus one) to split while resizing.
 */
private transient volatile int transferIndex;

resize中要拆分的下一個表索引。

2.6 cellsBusy屬性

/**
 * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
 */
private transient volatile int cellsBusy;

resize過程和/或建立CounterCells過程中使用的自旋鎖。

2.7 counterCells陣列

/**
 * Table of counter cells. When non-null, size is a power of 2.
 */
private transient volatile CounterCell[] counterCells;

顯然,這是CounterCell的陣列,即計數單元的陣列。

3. 內部類

3.1 Node內部類

Node內部類是ConcurrentHashMap類中普通節點的抽象。

/**
 * Key-value entry.  This class is never exported out as a
 * user-mutable Map.Entry (i.e., one supporting setValue; see
 * MapEntry below), but can be used for read-only traversals used
 * in bulk tasks.  Subclasses of Node with a negative hash field
 * are special, and contain null keys and values (but are never
 * exported).  Otherwise, keys and vals are never null.
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

    Node(int hash, K key, V val) {
        this.hash = hash;
        this.key = key;
        this.val = val;
    }

    Node(int hash, K key, V val, Node<K,V> next) {
        this(hash, key, 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 Helpers.mapEntryToString(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)));
    }

    /**
     * Virtualized support for map.get(); overridden in subclasses.
     */
    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;
    }
}

意義

Node內部類作為ConcurrentHashMap節點的實現。

hashCode()的實現

注意hashCode()的實現方式:Objects.hashCode(key) ^ Objects.hashCode(value);

find()

這裡Node內部類的find()方法其實不會在一般的業務方法如get()中被呼叫,因為在那些地方會直接遍歷。這個方法會在ForwardingNode類的find()方法中被呼叫。

4. 工具方法

4.1 spread方法

/**
 * Spreads (XORs) higher bits of hash to lower and also forces top
 * bit to 0. Because the table uses power-of-two masking, sets of
 * hashes that vary only in bits above the current mask will
 * always collide. (Among known examples are sets of Float keys
 * holding consecutive whole numbers in small tables.)  So we
 * apply a transform that spreads the impact of higher bits
 * downward. There is a tradeoff between speed, utility, and
 * quality of bit-spreading. Because many common sets of hashes
 * are already reasonably distributed (so don't benefit from
 * spreading), and because we use trees to handle large sets of
 * collisions in bins, we just XOR some shifted bits in the
 * cheapest possible way to reduce systematic lossage, as well as
 * to incorporate impact of the highest bits that would otherwise
 * never be used in index calculations because of table bounds.
 */
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

通過取高位再進行掩碼計算(保證雜湊值為正),來減少雜湊衝突。

這個方法就是所謂的擾動方法

4.2 tableSizeFor方法

/**
 * Returns a power of two table size for the given desired capacity.
 * See Hackers Delight, sec 3.2
 */
private static final int tableSizeFor(int c) {
    int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor方法用於計算引數c對應的resize閾值。往往出現為下面的語句。

4.3 comparableClassFor方法

/**
 * Returns x's Class if it is of the form "class C implements
 * Comparable<C>", else null.
 */
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; ParameterizedType p;
        // 如果是String 直接返回
        if ((c = x.getClass()) == String.class) 
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            for (Type t : ts) {
                if ((t instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

如果引數x是一個Comparable介面的實現類,那麼返回它的型別。

4.4 compareComparables方法

/**
 * Returns k.compareTo(x) if x matches kc (k's screened comparable
 * class), else 0.
 */
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

如果物件x匹配k的可比類kc,那麼返回k.compareTo(x),否則返回0。

4.5 列表元素訪問方法

4.5.1 tabAt方法
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

tabAt()方法可以獲得在 i 位置上的 Node 節點。

4.5.2 casTabAt方法
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

casTabAt()方法可以以CAS的方式更新 i 位置上的 Node 節點

4.5.3 setTabAt方法
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}

setTabAt方法可以設定在 i 位置上的 Node 節點。

注:像Unsafe.getReferenceAcquire()方法和Unsafe.putReferenceRelease()方法這樣的方法實際上是Unsafevolatile方法的發行版本。如後者是putReferenceVolatile()的發行版本。

4.6 initTable方法

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,說明正在初始化或resize,自旋
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {// 如果SIZECTL仍是sc,則置為-1.表示進入初始化
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // 獲取初始大小(sc為正時即為初始大小)
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 建立一個node陣列
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 為table屬性賦值
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                // 最後記得更新sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

initTable()方法可以初始化一個空表。

4.7 hashCode方法

public int hashCode() {
    int h = 0;
    Node<K,V>[] t;
    if ((t = table) != null) {
        Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
        for (Node<K,V> p; (p = it.advance()) != null; )
            h += p.key.hashCode() ^ p.val.hashCode();
    }
    return h;
}

hashCode()方法就是遍歷每個鍵值對,令他們的鍵和值的雜湊碼相異或,再全部疊加起來。

4.8 addCount方法

addCount()方法會在ConcurrentHashMap元素數量變化時被呼叫,兩個引數中第一個為數量變化值,第二個為控制是否需要擴容檢查的引數。

private final void addCount(long x, int check) {
    // 建立計數單元CounterCell
    CounterCell[] cs; long b, s;
    /**
    	1.如果counterCells為null:
    	那麼說明之前沒有發生過併發衝突,隨後會執行U.compareAndSetLong(...,b+x),直接更新了計數值baseCount。而這個本地方法執行成功會返回true,取反後為false。那麼整個這個if判斷兩個條件都為false,不執行if塊內內容。
    	2.如果couterCells不為null:
    	那麼說明之前發生過併發衝突,需要下面的if塊處理。這裡if的第一個條件為ture,第二個條件的更新方法就不會執行。
    */
    if ((cs = counterCells) != null 
        || !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 進入if塊,說明曾發生過併發衝突,那麼要把值加到CounterCell中
        CounterCell c; long v; int m;
        boolean uncontended = true;
		
        if (cs == null // cs在併發中又變成了null
            || (m = cs.length - 1) < 0 // cs長度小於1
            || (c = cs[ThreadLocalRandom.getProbe() & m]) == null // 對應的CouterCell為null
            || !(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {// 嘗試更新找到的計數單元c的值
            // 如果更新失敗。一般就是上面的最後一個條件中的方法返回了false,取反後為true
          	// 說明CounterCells陣列中出現了併發衝突,可能涉及該陣列的擴容,呼叫fullAddCount方法
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)// 如果無需檢查,直接返回
            return;
        // 計數,存到s中,下面用於做檢查
        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) << RESIZE_STAMP_SHIFT;
            // 如果正在執行resize
            if (sc < 0) {
                // 一些放棄幫助擴容的條件
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break;
                // 將sc+1,表示一個新執行緒加入幫助擴容
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 當前沒在執行resize,嘗試成為第一個進入擴容的執行緒。將sc設定為rs+2
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            // 重新計算元素數量
            s = sumCount();
        }
    }
}

詳細的邏輯看程式碼註釋,這裡單獨講幾個點。

  • 第一個if的判斷條件的使用很精彩,根據CounterCells陣列是否為null來檢查是該直接把值加到baseCount上還是加到對應的CounterCell中。
  • 注意在CounterCells陣列中找到槽位置的方式:c = cs[ThreadLocalRandom.getProbe() & m]) == null
  • check引數小於等於1時,不用做檢查就退出。大於1時,要在addCount的主邏輯結束之後在檢查需不需要做擴容。在put方法呼叫addCount時,傳入的check引數其實是put過程中遍歷到的節點數量,這樣邏輯就連通了:假如原先就只有一個節點或為空,就不需要考慮是否需要再檢查擴容;否則就要在addCoumt中檢查。

4.9 helpTransfer方法

helpTransfer方法可以在節點正在resize時協助資料遷移並返回新陣列。在putremove等業務方法中都有呼叫這個方法。

/**
 * Helps transfer if a resize is in progress.
 */
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)// f 是一個Forwarding Node
            && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) // nextTable不空
    {
        // 計算出本次resize時的標記“戳”
        int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        
        while (nextTab == nextTable // nextTab不變
               && table == tab // table不變
               && (sc = sizeCtl) < 0) // sizeCtl保持小於0(正在resize)
        {
            if (sc == rs + MAX_RESIZERS // 工作執行緒數量已滿
                || sc == rs + 1 // 在addCount方法中,假如有第一個擴容執行緒,sc=rs+2。假如變成rs+1,說明擴容結束。
                || transferIndex <= 0) // 如果transferIndex小於等於0,實際說明擴容工作已完成,已進入下標調整。
                break;
            // 令sc++,進入擴容
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        // 返回新表
        return nextTab;
    }
    // 返回原表
    return table;
}

4.10 transfer方法

transfer將方法的功能是將每個 bin 中的節點移動和/或複製到新表。 在addCount()helpTransfer()中都有呼叫,是擴容工作的核心實現類。

下面例子中若出現具體數字,以傳入tab的length為16為準。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  	// 定義n為表長。
    int n = tab.length, stride;
    /**
      stride表示一次transfer中一個工作執行緒的任務量,即要處理的連續雜湊桶的數目。
      初始化stride:假如可用CPU核數大於1,初始化為(n >>> 3) / NCPU,否則初始化為n。
      假如初始化後的stride小於MIN_TRANSFER_STRIDE,把他置為這個最小值。
    */
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    
    if (nextTab == null) { // nextTab未初始化,就要先初始化這個陣列
        try {
            @SuppressWarnings("unchecked")‘
            // 建立nextTab陣列,長度為原陣列長度*2
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            // 建立新陣列失敗,sizeCtl就設為int的最大值
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 這個陣列賦值給nextTable
        nextTable = nextTab;
        // 更新轉移下標
        transferIndex = n;
    }
    int nextn = nextTab.length;
    // 建立ForwardingNode fwd,傳入nextTab作為引數
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 首次推進為 true,如果等於 true,說明需要再次推進一個下標(i--),反之,如果是 false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進
    boolean advance = true;
    // 標記是否已經擴容完成
    boolean finishing = false; // to ensure sweep before committing nextTab
    /**
      又是一個for迴圈自旋,處理每個槽位中的連結串列元素
    */
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        /** 
          這個while迴圈通過CAS不斷嘗試為當前執行緒分配任務,直到分配成功或任務佇列已經被全部分配完畢。
          如果執行緒已經被分配過bucket區域,那麼會通過--i指向下一個待處理桶然後退出迴圈。
    	*/
        while (advance) {
            int nextIndex, nextBound;
            // --i表示進入下一個待處理的bucket。自減後大於等於bound,表明當前執行緒已經分配過bucket,advance=false
            if (--i >= bound || finishing)
                advance = false;
            // 所有bucket已經被分配完畢,為nextIndex賦值。
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // CAS修改TRANSFERINDEX,為執行緒分配任務。
            // 處理的節點區間為(nextBound,nextINdex)
            else if (U.compareAndSetInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        
        // 處理過程
        // CASE1:舊陣列已遍歷完成,當前執行緒已經處理完所有負責的bucket
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 擴容已完成
            if (finishing) {
                // 刪除這個成員變數nextTable
                nextTable = null;
                // 更新陣列
                table = nextTab;
                // 更新擴容閾值
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 使用 CAS 操作對 sizeCtl 的低16位進行減 1,代表做完了屬於自己的任務
            if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 如果上面的if沒有執行,即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                // 說明當前已經沒有擴容執行緒了,擴容結束了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // CASE2:節點i處為空,那麼放入剛剛初始化的ForwardingNode
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // CASE3:該位置現在的雜湊值就是MOVED,是一個ForwardingNode,說明已經被其他執行緒處理過,那麼要求繼續推進
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        // CASE4:執行轉移
        else {
            // 鎖住頭節點
            synchronized (f) {
                // 再次檢查
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 槽中頭節點是一個連結串列頭節點
                    if (fh >= 0) {
                        // 先計算好當前的fh * n
                        int runBit = fh & n;
                        // 儲存遍歷最終位置的lastRun
                        Node<K,V> lastRun = f;
                        // 遍歷連結串列
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            // 如果遍歷過程中出現hash&n出現了變化,需要更新runBit和lastRun
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        
                        //如果lastRun引用的是低位連結串列,令ln為lastRun
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        // 如果lastRUn引用的是高位連結串列,令hn為lastRun
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        
                        // 遍歷連結串列,把hash&n為0的放在低位連結串列中,不為0的放在高位連結串列中
                        // 迴圈跳出條件:當前迴圈結點!=lastRun
                        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);
                        // 高位連結串列的位置是:原位置 + n
                        setTabAt(nextTab, i + n, hn);
                        // 標記當前桶已遷移
                        setTabAt(tab, i, fwd);
                        // advance為true,返回上面進行--i操作
                        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");
                }
            }
        }
    }
}

transfer()方法是ConcurrentHashMap執行擴容操作的核心方法,他的擴容轉移操作其實類似於HashMap,都是將原連結串列分裂為兩個連結串列。

但也有很多實現細節上的不同,詳見原始碼註釋。

4.11 resizeStamp方法

/**
 * Returns the stamp bits for resizing a table of size n.
 * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
 */
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

resizeStamp(int n)方法可以在擴容一個大小為n的table時,計算stamp bits

5. 業務方法

5.1 構造方法

// 預設構造方法
public ConcurrentHashMap() {
}

// 僅提供初始容量的構造方法
public ConcurrentHashMap(int initialCapacity) {
    this(initialCapacity, LOAD_FACTOR, 1);
}

// 提供map的構造方法
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(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);
    // cap賦值為最大容量或擴容閾值
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

5.2 size方法

// 計數單元陣列
private transient volatile CounterCell[] counterCells;

public int size() {
    // 呼叫sumCount()
    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;
}

// 非常簡單的一個計數單元,僅有一個volatile型的計數器value
@jdk.internal.vm.annotation.Contended // 這個註解可以保證當前類的物件獨享快取行
static final class CounterCell {
    // 只提供了構造器,沒有提供get/set方法,也就是value的值在初始化時確定,後續不再更改
    volatile long value;
    CounterCell(long x) { value = x; }
}

size()方法的實現是首先獲取baseCount,這是無爭用時獲取的計數器值。再將計數單元陣列中的計數值累加在上面。他有以下幾個保證執行緒安全的舉措:

  • counterCells陣列和CounterCell類中的value變數設為volatile
  • 沒有為CounterCell類中的value變數設定get/set方法。

那麼CounterCells陣列是怎麼被建立和初始化的呢,baseCount是怎麼增加的呢。後續結合到那些改變了size的業務方法,如put()等方法的原始碼,我們再來做解釋。

5.3 isEmpty方法

public boolean isEmpty() {
    return sumCount() <= 0L; // ignore transient negative values
}

sumCount()方法見5.2

5.4 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 // 表長度不為0
			&& (e = tabAt(tab, (n - 1) & h)) != null) {// 指定位置不為null
        // 首位置即為要找的key
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        
        else if (eh < 0)// 當前連結串列頭雜湊值小於0,說明是一個特殊節點
            // 呼叫特殊節點e的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在雜湊表中的雜湊位置。然後根據找到的節點的雜湊值的不同,來做不同的處理。

  • 如果雜湊值時所要找到的值,直接返回。
  • 如果雜湊值小於0,代表當前節點是一個特殊節點。可參考1.13 特殊節點的hash狀態引數。這樣的話就會去呼叫特殊節點的find()方法,如ForwardingNode類和TreeNode類的find()方法。
  • 如果雜湊值大於等於0,遍歷當前連結串列查詢。

5.5 containsKey方法

public boolean containsKey(Object key) {
    return get(key) != null;
}

5.6 containsValue方法

public boolean containsValue(Object value) {
    if (value == null)
        throw new NullPointerException();
    Node<K,V>[] t;
    if ((t = table) != null) {
        Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
        for (Node<K,V> p; (p = it.advance()) != null; ) {
            V v;
            if ((v = p.val) == value || (v != null && value.equals(v)))
                return true;
        }
    }
    return false;
}

Traverser類封裝了containsValue方法的遍歷邏輯,程式碼較為複雜,這裡暫且按下不表。

5.7 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());
    // 當前桶的計數器
    int binCount = 0;
    // 自旋插入節點,直到成功
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        // CASE1:表為空則先呼叫初始化方法
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // CASE2:如果雜湊位置節點為空,向空位置插入時是無鎖的
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 嘗試把要put的鍵值對直接put到這裡
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;// 退出
        }
        // CASE3:如果雜湊位置節點雜湊值為-1,即為一個Forwarding Node,呼叫helperTransfer()
        else if ((fh = f.hash) == MOVED)
            // 協助轉移資料並獲取新陣列
            tab = helpTransfer(tab, f);
        // CASE4:如果onlyIfAbsent為true,且頭節點即為所需節點,直接返回它
        else if (onlyIfAbsent
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        // CASE5:找到了指定位置,並且不為空(出現了雜湊衝突)。
        else {
            V oldVal = null;
            synchronized (f) {// 鎖住當前節點(連結串列頭)
                if (tabAt(tab, i) == f) {// 再判斷一下f是不是頭節點,防止它被別的執行緒已經修改了
                    // if-不是一個特殊節點
                    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;
                            }
                            // 如果到了尾部,就插入由當前key-value構建的新節點
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value);
                                break;
                            }
                        }
                    }
                    // elseIf-是一個樹節點
                    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-如果是一個保留節點
                    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);
    // 返回null
    return null;
}

具體邏輯見註釋,解釋得很詳細。

putVal方法會用一個for迴圈保持自旋,不斷嘗試插入要求插入的鍵值對。迴圈內有以下幾個CASE,體現為if-else塊中的五個分支。

  • 表為空。呼叫初始化方法。
  • 雜湊位置為空。無鎖地直接put。
  • 雜湊位置是一個ForwardingNode,呼叫helpTransfer
  • 雜湊位置頭即為當前鍵,且onlyIfAbsenttrue,直接返回。
  • 雜湊位置不為空,說明出現了雜湊衝突。

注意在遍歷過程中對binCount的更新,最終用addCount()令物件加一,並用binCount作為check引數。

5.8 remove方法

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

final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    // 自旋
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // CASE1:可以直接退出的情況:陣列為空或雜湊結果位置為null。
        if (tab == null 
            || (n = tab.length) == 0 
            || (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        // CASE2:節點正在移動,協助移動
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // CASE3:出現雜湊衝突,要在連結串列中查詢
        else {
            V oldVal = null;
            boolean validated = false;
            // 鎖住頭節點
            synchronized (f) {// 內部具體邏輯不再贅述,類似於上面的put方法
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        validated = true;
                        // e 表示當前迴圈處理結點,pred 表示當前迴圈節點的上一個節點
                        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);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

關鍵還是在於鎖住連結串列頭來實現執行緒安全。邏輯直接看原始碼即可。

相關文章