ConcurrentHashMap原始碼解讀一

=凌晨=發表於2021-05-10

最近在學習併發map的原始碼,如果由錯誤歡迎指出。這僅供我自己學習記錄使用。

首先就先來說一下幾個全域性變數

    private static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量2的30次方
    private static final int DEFAULT_CAPACITY = 16; //預設容量  1<<4
    
    private static final float LOAD_FACTOR = 0.75f;  //負載因子
    static final int TREEIFY_THRESHOLD = 8;  //連結串列轉為紅黑樹,大於8小於6先對連結串列陣列進行翻倍擴容操作
    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;//stamp高位標識移動位數
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    static final int MOVED     = -1; // forwarding nodes 的hash值,如果hash值等於-1代表執行緒協助擴容
    static final int TREEBIN   = -2; // roots of trees 的hash值,如果hash等於-2代表,當前桶是紅黑樹
    static final int RESERVED  = -3; // transient reservations 的hash值

    // usable bits of normal node hash,在hash計算的時候運用到,與HashMap計算出來的hash值進行與操作
    static final int HASH_BITS = 0x7fffffff; 
    
    static final int NCPU = Runtime.getRuntime().availableProcessors(); //可用處理器數量       

然後是幾個全域性屬性

transient volatile Node<K,V>[] table;//當前ConcurrentHashmap的Node陣列,正在使用的陣列

private transient volatile Node<K,V>[] nextTable;//ForwardNode所指向的下一個表,正在擴容的陣列(還未使用)


private transient volatile long baseCount;//如果使用CAS計數成功,使用該值進行累加,計數用的

//擴容設定的引數,預設為0,當值=-1的時候,代表當前有執行緒正在進行擴容操作
//當值等於-n的時候,代表有n個執行緒一起擴容,其中n-1執行緒是協助擴容
//當在初始化的時候指定了大小,這會將這個大小儲存在sizeCtl中,大小為陣列的0.75
private transient volatile int sizeCtl;//標記狀態以及陣列閾值


private transient volatile int transferIndex;//陣列擴容的時候用到


private transient volatile int cellsBusy;

//如果使用CAS計算失敗,也就是說當前處於高併發的情況下,那麼
//就會使用CounterCell[]陣列進行計數,類似jdk1.7分段鎖的形式,鎖住一個segment
//最後size()方法統計出來的大小是baseCount和counterCells陣列的總和
private transient volatile CounterCell[] counterCells;//計數陣列。

首先是有參構造,這裡如果是

ConcurrentHashMap chm = new ConcurrentHashMap(15);

那麼其實容量不是15,而是32;

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

從這裡可以看出是看tableSizeFor這個方法的,15+7+1=23;

 private static final int tableSizeFor(int c) {
        int n = c - 1;//23-1=22  0b10110
        n |= n >>> 1;// 10110 | 01011 = 11111,下面都是11111也就是31
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//31+1 =  32
    }

所以這就是最後的容量,為32,也就是有參的引數的兩倍最近的2的次方數。

接下來就將put方法

final V putVal(K key, V value, boolean onlyIfAbsent) {//onlyIfAbsent跟hashmap一樣,就是判斷是否要覆蓋,預設為false,覆蓋。
        if (key == null || value == null) throw new NullPointerException();////這句話可以看出,ConcurrentHashMap中不允許存在空值,這個是跟HashMap的區別之一 //通過這個機制,我們可以通過get方法獲取一個key,如果丟擲異常,說明這個key不存在
        int hash = spread(key.hashCode());//這個方法就相當於基於計算hash值。
        int binCount = 0;//這個是記錄這個桶的元素個數,目的是用它來判斷是否需要轉換紅黑樹,
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
        //情況一:如果陣列為空或者長度為0,進行初始化工作
if (tab == null || (n = tab.length) == 0) tab = initTable();
        //情況二:如果獲取的位置的節點為空,說明是首節點插入情況,也就是該桶位置沒有元素,利用cas將元素新增。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null,//cas加自旋(和外側的for構成自旋迴圈),保證元素新增安全 new Node<K,V>(hash, key, value, null)))//如果加成功了,那麼就break,否則再經過for的死迴圈進行判斷 break; // no lock when adding to empty bin }
        //情況三:如果hash計算得到的桶的位置元素的hash值為moved,也就是-1,證明正在擴容,那麼就協助擴容。
else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);
         //hash計算的桶位置元素不為空,且當前沒有處於擴容操作,進行元素新增
        //情況四:這個桶有元素,則執行插入操作,有兩種可能,一是這個桶對應的連結串列沒有相同的key,那麼久再連結串列尾插入node節點,而是有相同的key,那麼久替換其value。
else { V oldVal = null; synchronized (f) { //對當前桶進行加鎖,保證執行緒安全,執行元素新增操作 //將桶位置的元素鎖住,那麼在該桶位中的連結串列或者紅黑樹進行新增元素的話,就是安全的,只有這個執行緒拿住了這個鎖 if (tabAt(tab, i) == f) {//因為新增元素之後可能連結串列已經變成紅黑樹了,那麼這個f就可能變化了。所以要再進行判斷。 if (fh >= 0) {//說明是普通連結串列節點 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {//進行節點判斷,如果都一樣,那麼覆蓋舊值。 oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) {//尾插法,如果e的下一個不是null,那麼迴圈會讓pred變成e,直到最後節點,此時e的下一個為null的話
                            //那麼也就是pred下一個為null,那麼插入到pred下一個即可。 pred.next
= new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { //樹節點,將元素新增到紅黑樹中 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {  oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//連結串列長度大於/等於8,有可能將連結串列轉成紅黑樹,因為在treeifyBin(tab, i);方法中還有一個判斷陣列長度是否小於64的判斷,如果小於64,就不會
                //樹化。只是陣列擴容。
treeifyBin(tab, i);
if (oldVal != null) //如果是重複鍵,直接將舊值返回 return oldVal; break; } } } addCount(1L, binCount);//新增的是新元素,維護集合長度,並判斷是否要進行擴容操作 return null; }

總結:併發map,jdk1.8的情況下,底層跟hashmap一樣,也是陣列加連結串列加紅黑樹。

相關文章