最近在學習併發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一樣,也是陣列加連結串列加紅黑樹。