1. 程式人生 > 程式設計 >J.U.C 之ConcurrentHashMap(JDK1.8)

J.U.C 之ConcurrentHashMap(JDK1.8)

在1.8版本以前,ConcurrentHashMap採用分段鎖的概念,使鎖更加細化,但是1.8已經改變了這種思路,而是利用CAS+Synchronized來保證併發更新的安全,當然底層採用陣列+連結串列+紅黑樹的儲存結構。

重要概念

ConcurrentHashMap定義瞭如下幾個常量:

// 最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;

// 預設初始值,必須是2的幕數
private static final int DEFAULT_CAPACITY = 16;

//
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(); 複製程式碼

上面是ConcurrentHashMap定義的常量。下面介紹ConcurrentHashMap幾個很重要的概念

  1. table:用來存放Node節點資料的,預設為null,預設大小為16的陣列,每次擴容時大小總是2的冪次方;

  2. nextTable:擴容時新生成的資料,陣列為table的兩倍;

  3. Node:節點,儲存key-value的資料結構;

  4. ForwardingNode:一個特殊的Node節點,hash值為-1,其中儲存nextTable的引用。只有table發生擴容的時候,ForwardingNode才會發揮作用,作為一個佔位符放在table中表示當前節點為null或則已經被移動

  5. sizeCtl:控制識別符號,用來控制table初始化和擴容操作的,在不同的地方有不同的用途,其值也不同,所代表的含義也不同

     負數代表正在進行初始化或擴容操作
     -1代表正在初始化
     -N 表示有N-1個執行緒正在進行擴容操作
     正數或0代表hash表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小
    複製程式碼

重要內部類

為了實現ConcurrentHashMap,Doug Lea提供了許多內部類來進行輔助實現,如Node,TreeNode,TreeBin等等。

Node

作為ConcurrentHashMap中最核心、最重要的內部類,Node擔負著重要角色:key-value鍵值對。所有插入ConCurrentHashMap的中資料都將會包裝在Node中。定義如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;             //帶有volatile,保證可見性
    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)));
    }

    /**  賦值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;
    }
}
複製程式碼

在Node內部類中,其屬性value、next都是帶有volatile的。同時其對value的setter方法進行了特殊處理(丟擲UnsupportedOperationException異常),不允許直接呼叫其setter方法來修改value的值。最後Node還提供了find方法來賦值map.get()。

TreeNode

我們知道HashMap的核心資料結構就是連結串列。在ConcurrentHashMap中就不一樣了,如果連結串列的資料過長是會轉換為紅黑樹來處理。它並不是直接轉換,而是將這些連結串列的節點包裝成TreeNode放在TreeBin物件中,然後由TreeBin完成紅黑樹的轉換。所以TreeNode也必須是ConcurrentHashMap的一個核心類,其為樹節點類,定義如下:

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,V> next,TreeNode<K,V> parent) {
            super(hash,key,val,next);
            this.parent = parent;
        }


        Node<K,Object k) {
            return findTreeNode(h,k,null);
        }

        //查詢hash為h,key為k的節點
        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,pk)) != 0)
                        p = (dir < 0) ? pl : pr;
                    else if ((q = pr.findTreeNode(h,kc)) != null)
                        return q;
                    else
                        p = pl;
                } while (p != null);
            }
            return null;
        }
    }
複製程式碼

原始碼展示TreeNode繼承Node,且提供了findTreeNode用來查詢查詢hash為h,key為k的節點。

TreeBin

該類並不負責key-value的鍵值對包裝,它用於在連結串列轉換為紅黑樹時包裝TreeNode節點,也就是說ConcurrentHashMap紅黑樹存放是TreeBin,不是TreeNode。

該類封裝了一系列的方法,包括putTreeVal、lookRoot、UNlookRoot、remove、balanceInsetion、balanceDeletion。由於TreeBin的程式碼太長我們這裡只展示構造方法(構造方法就是構造紅黑樹的過程):

static final class TreeBin<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    volatile Thread waiter;
    volatile int 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

    TreeBin(TreeNode<K,V> b) {
        super(TREEBIN,null,null);
        this.first = b;
        TreeNode<K,V> r = null;
        for (TreeNode<K,V> x = b,next; x != null; x = next) {
            next = (TreeNode<K,V>) x.next;
            x.left = x.right = null;
            if (r == null) {
                x.parent = null;
                x.red = false;
                r = x;
            } else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (TreeNode<K,V> p = r; ; ) {
                    int dir,ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc,pk)) == 0)
                        dir = tieBreakOrder(k,pk);
                    TreeNode<K,V> xp = p;
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        r = balanceInsertion(r,x);
                        break;
                    }
                }
            }
        }
        this.root = r;
        assert checkInvariants(root);
    }

    /** 省略很多程式碼 */
}
複製程式碼

通過構造方法是不是發現了部分端倪,構造方法就是在構造一個紅黑樹的過程。

ForwardingNode

這是一個真正的輔助類,該類僅僅只存活在ConcurrentHashMap擴容操作時。只是一個標誌節點,並且指向nextTable,它提供find方法而已。該類也是整合Node節點,其hash為-1,key、value、next均為null。如下:

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

       Node<K,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;
               }
           }
       }
   }
複製程式碼

主要方法

建構函式

ConcurrentHashMap提供了一系列的建構函式用於建立ConcurrentHashMap物件:

public ConcurrentHashMap() {
}

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

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);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}
複製程式碼

初始化: initTable()

ConcurrentHashMap的初始化主要由initTable()方法實現,在上面的建構函式中我們可以看到,其實ConcurrentHashMap在建構函式中並沒有做什麼事,僅僅只是設定了一些引數而已。其真正的初始化是發生在插入的時候,例如put、merge、compute、computeIfAbsent、computeIfPresent操作時。其方法定義如下:

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        //sizeCtl < 0 表示有其他執行緒在初始化,該執行緒必須掛起
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        // 如果該執行緒獲取了初始化的權利,則用CAS將sizeCtl設定為-1,表示本執行緒正在初始化
        else if (U.compareAndSwapInt(this,SIZECTL,sc,-1)) {
                // 進行初始化
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 下次擴容的大小
                    sc = n - (n >>> 2); ///相當於0.75*n 設定一個擴容的閾值
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
複製程式碼

初始化方法initTable()的關鍵就在於sizeCtl,該值預設為0,如果在建構函式時有引數傳入該值則為2的冪次方。該值如果 < 0,表示有其他執行緒正在初始化,則必須暫停該執行緒。如果執行緒獲得了初始化的許可權則先將sizeCtl設定為-1,防止有其他執行緒進入,最後將sizeCtl設定0.75 * n,表示擴容的閾值。

put操作

ConcurrentHashMap最常用的put、get操作,ConcurrentHashMap的put操作與HashMap並沒有多大區別,其核心思想依然是根據hash值計算節點插入在table的位置,如果該位置為空,則直接插入,否則插入到連結串列或者樹中。但是ConcurrentHashMap會涉及到多執行緒情況就會複雜很多。我們先看原始碼,然後根據原始碼一步一步分析:

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

    final V putVal(K key,V value,boolean onlyIfAbsent) {
        //key、value均不能為null
        if (key == null || value == null) throw new NullPointerException();
        //計算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n,i,fh;
            // table為null,進行初始化工作
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果i位置沒有節點,則直接插入,不需要加鎖
            else if ((f = tabAt(tab,i = (n - 1) & hash)) == null) {
                if (casTabAt(tab,new Node<K,V>(hash,null)))
                    break;                   // no lock when adding to empty bin
            }
            // 有執行緒正在進行擴容操作,則先幫助擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab,f);
            else {
                V oldVal = null;
                //對該節點進行加鎖處理(hash值相同的連結串列的頭節點),對效能有點兒影響
                synchronized (f) {
                    if (tabAt(tab,i) == f) {
                        //fh > 0 表示為連結串列,將該節點插入到連結串列尾部
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //hash 和 key 都一樣,替換value
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //putIfAbsent()
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //連結串列尾部  直接插入
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,null);
                                    break;
                                }
                            }
                        }
                        //樹節點,按照樹的插入操作進行插入
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash,value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 如果連結串列長度已經達到臨界值8 就需要把連結串列轉換為樹結構
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab,i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }

        //size + 1
        addCount(1L,binCount);
        return null;
    }
複製程式碼

按照上面的原始碼,我們可以確定put整個流程如下:

  1. 判空;ConcurrentHashMap的key、value都不允許為null

  2. 計算hash。利用方法計算hash值。

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
複製程式碼
  1. 遍歷table,進行節點插入操作,過程如下:

    • 如果table為空,則表示ConcurrentHashMap還沒有初始化,則進行初始化操作:initTable()
    • 根據hash值獲取節點的位置i,若該位置為空,則直接插入,這個過程是不需要加鎖的。計算f位置:i=(n - 1) & hash 如果檢測到fh = f.hash == -1,則f是ForwardingNode節點,表示有其他執行緒正在進行擴容操作,則幫助執行緒一起進行擴容操作 如果f.hash >= 0 表示是連結串列結構,則遍歷連結串列,如果存在當前key節點則替換value,否則插入到連結串列尾部。如果f是TreeBin型別節點,則按照紅黑樹的方法更新或者增加節點 若連結串列長度 > TREEIFY_THRESHOLD(預設是8),則將連結串列轉換為紅黑樹結構
  2. 呼叫addCount方法,ConcurrentHashMap的size + 1

這裡整個put操作已經完成。

get操作

ConcurrentHashMap的get操作還是挺簡單的,無非就是通過hash來找key相同的節點而已,當然需要區分連結串列和樹形兩種情況。

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());
    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;
        }
        // 樹
        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;
}
複製程式碼

get操作的整個邏輯非常清楚:

  1. 計算hash值
  2. 判斷table是否為空,如果為空,直接返回null
  3. 根據hash值獲取table中的Node節點(tabAt(tab,(n - 1) & h)),然後根據連結串列或者樹形方式找到相對應的節點,返回其value值。

size 操作

ConcurrentHashMap的size()方法返回的是一個不精確的值,因為在進行統計的時候有其他執行緒正在進行插入和刪除操作。

為了更好地統計size,ConcurrentHashMap提供了 baseCount、counterCells 兩個輔助變數和一個CounterCell輔助內部類。

   @sun.misc.Contended static final class CounterCell {
       volatile long value;
       CounterCell(long x) { value = x; }
   }

//ConcurrentHashMap中元素個數,但返回的不一定是當前Map的真實元素個數。基於CAS無鎖更新
   private transient volatile long baseCount;

   private transient volatile CounterCell[] counterCells;
複製程式碼

這裡我們需要清楚CounterCell 的定義

size()方法定義如下:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
複製程式碼

內部呼叫sunmCount():

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            //遍歷,所有counter求和
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
複製程式碼

sumCount()就是迭代counterCells來統計sum的過程。我們知道put操作時,肯定會影響size(),我們就來看看CouncurrentHashMap是如何為了這個不和諧的size()操碎了心。

在put()方法最後會呼叫addCount()方法,該方法主要做兩件事,一件更新baseCount的值,第二件檢測是否進行擴容,我們只看更新baseCount部分:

  private final void addCount(long x,int check) {
      CounterCell[] as; long b,s;
      // s = b + x,完成baseCount++操作;
      if ((as = counterCells) != null ||
          !U.compareAndSwapLong(this,BASECOUNT,b = baseCount,s = b + x)) {
          CounterCell a; long v; int m;
          boolean uncontended = true;
          if (as == null || (m = as.length - 1) < 0 ||
              (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
              !(uncontended =
                U.compareAndSwapLong(a,CELLVALUE,v = a.value,v + x))) {
              //  多執行緒CAS發生失敗時執行
              fullAddCount(x,uncontended);
              return;
          }
          if (check <= 1)
              return;
          s = sumCount();
      }

// 檢查是否進行擴容
  }
複製程式碼

x == 1,如果counterCells == null,則U.compareAndSwapLong(this,s = b + x),如果併發競爭比較大可能會導致改過程失敗,如果失敗則最終會呼叫fullAddCount()方法。

其實為了提高高併發的時候baseCount可見性的失敗問題,又避免一直重試,JDK 8 引入了類Striped64,其中LongAdder和DoubleAdder都是基於該類實現的,而 CounterCell 也是基於Striped64實現的。如果counterCells != null,且uncontended = U.compareAndSwapLong(a,v + x)也失敗了,同樣會呼叫fullAddCount()方法,最後呼叫sumCount()計算s。

其實在1.8中,它不推薦size()方法,而是推崇mappingCount()方法,該方法的定義和size()方法基本一致:

public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // ignore transient negative values
}
複製程式碼

擴容操作

當ConcurrentHashMap中table元素個數達到了容量閾值(sizeCtl)時,則需要進行擴容操作。在put操作時最後一個會呼叫addCount(long x,int check),該方法主要做兩個工作:1.更新baseCount;2.檢測是否需要擴容操作。如下:

private final void addCount(long x,int check) {
    CounterCell[] as; long b,s;
    // 更新baseCount

    //check >= 0 :則需要進行擴容操作
    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,sc + 1))
                    transfer(tab,nt);
            }

            //當前執行緒是唯一的或是第一個發起擴容的執行緒  此時nextTable=null
            else if (U.compareAndSwapInt(this,(rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab,null);
            s = sumCount();
        }
    }
}
複製程式碼

transfer()方法為ConcurrentHashMap擴容操作的核心方法。由於ConcurrentHashMap支援多執行緒擴容,而且也沒有進行加鎖,所以實現會變得有點兒複雜。整個擴容操作分為兩步:

  1. 構建一個nextTable,其大小為原來大小的兩倍,這個步驟是在單執行緒環境下完成的
  2. 將原來table裡面的內容複製到nextTable中,這個步驟是允許多執行緒操作的,所以效能得到提升,減少了擴容的時間消耗

我們先來看看原始碼,然後再一步一步分析:

private final void transfer(Node<K,V>[] nextTab) {
      int n = tab.length,stride;
      // 每核處理的量小於16,則強制賦值16
      if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
          stride = MIN_TRANSFER_STRIDE; // subdivide range
      if (nextTab == null) {            // initiating
          try {
              @SuppressWarnings("unchecked")
              Node<K,?>[n << 1];        //構建一個nextTable物件,其容量為原來容量的兩倍
              nextTab = nt;
          } catch (Throwable ex) {      // try to cope with OOME
              sizeCtl = Integer.MAX_VALUE;
              return;
          }
          nextTable = nextTab;
          transferIndex = n;
      }
      int nextn = nextTab.length;
      // 連線點指標,用於標誌位(fwd的hash值為-1,fwd.nextTable=nextTab)
      ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
      // 當advance == true時,表明該節點已經處理過了
      boolean advance = true;
      boolean finishing = false; // to ensure sweep before committing nextTab
      for (int i = 0,bound = 0;;) {
          Node<K,V> f; int fh;
          // 控制 --i,遍歷原hash表中的節點
          while (advance) {
              int nextIndex,nextBound;
              if (--i >= bound || finishing)
                  advance = false;
              else if ((nextIndex = transferIndex) <= 0) {
                  i = -1;
                  advance = false;
              }
              // 用CAS計算得到的transferIndex
              else if (U.compareAndSwapInt
                      (this,TRANSFERINDEX,nextIndex,nextBound = (nextIndex > stride ?
                                      nextIndex - stride : 0))) {
                  bound = nextBound;
                  i = nextIndex - 1;
                  advance = false;
              }
          }
          if (i < 0 || i >= n || i + n >= nextn) {
              int sc;
              // 已經完成所有節點複製了
              if (finishing) {
                  nextTable = null;
                  table = nextTab;        // table 指向nextTable
                  sizeCtl = (n << 1) - (n >>> 1);     // sizeCtl閾值為原來的1.5倍
                  return;     // 跳出死迴圈,
              }
              // CAS 更擴容閾值,在這裡面sizectl值減一,說明新加入一個執行緒參與到擴容操作
              if (U.compareAndSwapInt(this,sc = sizeCtl,sc - 1)) {
                  if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                      return;
                  finishing = advance = true;
                  i = n; // recheck before commit
              }
          }
          // 遍歷的節點為null,則放入到ForwardingNode 指標節點
          else if ((f = tabAt(tab,i)) == null)
              advance = casTabAt(tab,fwd);
          // f.hash == -1 表示遍歷到了ForwardingNode節點,意味著該節點已經處理過了
          // 這裡是控制併發擴容的核心
          else if ((fh = f.hash) == MOVED)
              advance = true; // already processed
          else {
              // 節點加鎖
              synchronized (f) {
                  // 節點複製工作
                  if (tabAt(tab,i) == f) {
                      Node<K,V> ln,hn;
                      // fh >= 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,hn);
                          }
                          // 在nextTable i 位置處插上鍊表
                          setTabAt(nextTab,ln);
                          // 在nextTable i + n 位置處插上鍊表
                          setTabAt(nextTab,i + n,hn);
                          // 在table i 位置處插上ForwardingNode 表示該節點已經處理過了
                          setTabAt(tab,fwd);
                          // advance = true 可以執行--i動作,遍歷節點
                          advance = true;
                      }
                      // 如果是TreeBin,則按照紅黑樹進行處理,處理邏輯與上面一致
                      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);
                              if ((h & n) == 0) {
                                  if ((p.prev = loTail) == null)
                                      lo = p;
                                  else
                                      loTail.next = p;
                                  loTail = p;
                                  ++lc;
                              }
                              else {
                                  if ((p.prev = hiTail) == null)
                                      hi = p;
                                  else
                                      hiTail.next = p;
                                  hiTail = p;
                                  ++hc;
                              }
                          }

                          // 擴容後樹節點個數若<=6,將樹轉連結串列
                          ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                  (hc != 0) ? new TreeBin<K,V>(lo) : t;
                          hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                  (lc != 0) ? new TreeBin<K,V>(hi) : t;
                          setTabAt(nextTab,ln);
                          setTabAt(nextTab,hn);
                          setTabAt(tab,fwd);
                          advance = true;
                      }
                  }
              }
          }
      }
  }
複製程式碼

上面的原始碼有點兒長,稍微複雜了一些,在這裡我們拋棄它多執行緒環境,我們從單執行緒角度來看:

  1. 為每個核心分任務,並保證其不小於16
  2. 檢查nextTable是否為null,如果是,則初始化nextTable,使其容量為table的兩倍
  3. 死迴圈遍歷節點,知道finished:節點從table複製到nextTable中,支援併發,請思路如下:
    • 如果節點 f 為null,則插入ForwardingNode(採用Unsafe.compareAndSwapObjectf方法實現),這個是觸發併發擴容的關鍵
    • 如果f為連結串列的頭節點(fh >= 0),則先構造一個反序連結串列,然後把他們分別放在nextTable的i和i + n位置,並將ForwardingNode 插入原節點位置,代表已經處理過了
    • 如果f為TreeBin節點,同樣也是構造一個反序 ,同時需要判斷是否需要進行unTreeify()操作,並把處理的結果分別插入到nextTable的i 和i+nw位置,並插入ForwardingNode 節點
  4. 所有節點複製完成後,則將table指向nextTable,同時更新sizeCtl = nextTable的0.75倍,完成擴容過程

在多執行緒環境下,ConcurrentHashMap用兩點來保證正確性:ForwardingNode和synchronized。當一個執行緒遍歷到的節點如果是ForwardingNode,則繼續往後遍歷,如果不是,則將該節點加鎖,防止其他執行緒進入,完成後設定ForwardingNode節點,以便要其他執行緒可以看到該節點已經處理過了,如此交叉進行,高效而又安全。

下圖是擴容的過程:

在put操作時如果發現fh.hash = -1,則表示正在進行擴容操作,則當前執行緒會協助進行擴容操作。

else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab,f);
複製程式碼

helpTransfer()方法為協助擴容方法,當呼叫該方法的時候,nextTable一定已經建立了,所以該方法主要則是進行復制工作。如下:

final Node<K,V>[] helpTransfer(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,sc + 1)) {
                transfer(tab,nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
複製程式碼

轉換紅黑樹

在put操作是,如果發現連結串列結構中的元素超過了TREEIFY_THRESHOLD(預設為8),則會把連結串列轉換為紅黑樹,已便於提高查詢效率。如下:

if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab,i);
複製程式碼

呼叫treeifyBin方法用與將連結串列轉換為紅黑樹。

private final void treeifyBin(Node<K,int index) {
        Node<K,V> b; int n,sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)//如果table.length<64 就擴大一倍 返回
                tryPresize(n << 1);
            else if ((b = tabAt(tab,index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab,index) == b) {
                        TreeNode<K,V> hd = null,tl = null;
                        //構造了一個TreeBin物件 把所有Node節點包裝成TreeNode放進去
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash,null);//這裡只是利用了TreeNode封裝 而沒有利用TreeNode的next域和parent域
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //在原來index的位置 用TreeBin替換掉原來的Node物件
                        setTabAt(tab,index,new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }
複製程式碼

從上面原始碼可以看出,構建紅黑樹的過程是同步的,進入同步後過程如下:

  1. 根據table中index位置Node連結串列,重新生成一個hd為頭結點的TreeNode
  2. 根據hd頭結點,生成TreeBin樹結構,並用TreeBin替換掉原來的Node物件。