一、概述
ConcurrentHashMap是java併發包下一個常用的併發集合類,在面試中經常會被問及,一般在講述了執行緒不安全的HashMap之後,面試官會問這個。
在這篇文章中,我會詳細分析ConcurrentHashMap中幾個重要API的底層原始碼和實現機制
本文將以JDK1.8版本進行講解
二、JDK1.7與1.8區別
1.8中去除了Segment+HashEntry+Unsafe的實現方式,改為Synchronized+CAS+Node+Unsafe的實現方式
Segment分段
ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列,同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
Segment內部資料結構
ConcurrentHashMap(1.7)使用了分段鎖的機制,將資料分成一段一段進行儲存,每個Segment對應一把鎖,當一個執行緒佔用鎖訪問一個Segment時,其他執行緒可以訪問其他的Segment,這就實現了真正意義上的併發。
在1.7版本中,定位到一個元素需要通過兩次Hash計算,第一次計算定位到Segment,第二次計算定位到HashEntry(所在元素的頭結點)
優缺點
優點:
在寫操作的時候只需要對當前的Segment加鎖即可,因此理想狀態下,ConcurrentHashMap同時可支援Segment數量的併發寫操作,而且不會影響到其他的Segment部分,通過這種資料結構可以大大提高併發力度。
缺點:
由於是通過Segment -》HashEntry的定位方式,需要兩次計算,相比1.8中效率比較低下
三、核心原始碼分析
1.ConcurrentHashMap中的Node節點
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, 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))); } /** * 比較常見的連結串列的搜尋方法,通過對比hash值,value來判斷是否是目標元素,如果不是的話就e = e.next向下遍歷,如果到末尾為null了 就返回null */ 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; } }
/** * Creates a new, empty map with the default initial table size (16). 如果是空構造器,預設初始長度是16 */ 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; }
如果制定了初始容量的大小,先進行小於零的驗證,判斷初始化大小是否大於最大值的一般,如果是的話就去最大時,否則就執行tableSizeFor方法,這個方法返回的是一個大於或者等於輸入的初始化容量的一個2^n的大小
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); //判斷K,V是否為空 int hash = spread(key.hashCode()); //計算key的hash值 int binCount = 0; //用來計算這個節點總共有多少元素,用來控制擴容或者轉化為紅黑樹 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); //如果當前的table沒有初始化,就先進行初始化 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // 通過CAS自旋的方式,將元素添封裝成Node,新增到這個位置,注意這時候是沒有加鎖的 } else if ((fh = f.hash) == MOVED) //判斷是否處於擴容階段,是的話就先幫助擴容 tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { //如果這個位置有元素,使用sunchronized鎖對當前頭節點進行加鎖 if (tabAt(tab, i) == 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; //如果存在這個Key,就把值進行替換 if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) {//如果遍歷到最後不存在得話,就把Key,value封裝成Node,放到尾部 pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { //如果是紅黑樹形式,使用putTreeVal方法進行新增 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) return oldVal; break; } } } addCount(1L, binCount);//計數 return null; }
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // 每個cpu至少處理16個長度的資料元素,用於控制不佔用過多cpu資源 if (nextTab == null) { // 如果第一個複製的目標是空的話,初始化一個table兩倍長的nexttable try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; } int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//fwd節點,用來控制併發,當一個節點為空或者已經被轉移了,就設定為fwd節點 boolean advance = true; //判斷是否繼續向前判斷,ConcurrentHashMap複製是從n號往0開始遍歷判斷 boolean finishing = false; // 重新掃描陣列用的,看看有沒有沒完成的 for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) //判斷已經到頭了 advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } 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; sizeCtl = (n << 1) - (n >>> 1); //設定閾值為擴容後的0.75 return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } else if ((f = tabAt(tab, i)) == null) //把陣列中null的元素設定為fwd節點 advance = casTabAt(tab, i, null, fwd); else if ((fh = f.hash) == MOVED) advance = true; // already processed else { synchronized (f) {//加鎖開始轉移 if (tabAt(tab, i) == f) { Node<K,V> ln, hn; if (fh >= 0) { //判斷是一個Node節點 int runBit = fh & n; //這裡是判斷&操作後是0還是1,因為n的值是陣列長度,是2的冪此,所以這個結果就是,如果是0就放在新表 的同一個位置,如果是1的話就放在新表原位置+n的地方 Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; //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) { //構造兩個連結串列,分別放到原來的位置和新增加的長度的相同位置,i 或者n+i 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); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); 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; } } //判斷複製完之後,如果節點數小於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, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }
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) { //判斷如果是頭節點,返回value 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; }
四、ConcurrentHashMap的同步機制
前面分析了下ConcurrentHashMap的原始碼,那麼,對於一個對映集合來說,ConcurrentHashMap是如果來做到併發安全,又是如何做到高效的併發的呢?
首先是讀操作,從原始碼中可以看出來,在get操作中,根本沒有使用同步機制,也沒有使用unsafe方法,所以讀操作是支援併發操作的。
那麼寫操作呢?
分析這個之前,先看看什麼情況下會引起陣列的擴容,擴容是通過transfer方法來進行的。而呼叫transfer方法的只有trePresize、helpTransfer和addCount三個方法。
這三個方法又是分別在什麼情況下進行呼叫的呢?
·tryPresize是在treeIfybin和putAll方法中呼叫,treeIfybin主要是在put新增元素完之後,判斷該陣列節點相關元素是不是已經超過8個的時候,如果超過則會呼叫這個方法來擴容陣列或者把連結串列轉為樹。
·helpTransfer是在當一個執行緒要對table中元素進行操作的時候,如果檢測到節點的HASH值為MOVED的時候,就會呼叫helpTransfer方法,在helpTransfer中再呼叫transfer方法來幫助完成陣列的擴容
·addCount是在當對陣列進行操作,使得陣列中儲存的元素個數發生了變化的時候會呼叫的方法。
所以引起陣列擴容的情況如下:
·只有在往map中新增元素的時候,在某一個節點的數目已經超過了8個,同時陣列的長度又小於64的時候,才會觸發陣列的擴容。
·當陣列中元素達到了sizeCtl的數量的時候,則會呼叫transfer方法來進行擴容
那麼在擴容的時候,可以不可以對陣列進行讀寫操作呢?
事實上是可以的。當在進行陣列擴容的時候,如果當前節點還沒有被處理(也就是說還沒有設定為fwd節點),那就可以進行設定操作。
如果該節點已經被處理了,則當前執行緒也會加入到擴容的操作中去。
那麼,多個執行緒又是如何同步處理的呢?
在ConcurrentHashMap中,同步處理主要是通過Synchronized和unsafe兩種方式來完成的。
·在取得sizeCtl、某個位置的Node的時候,使用的都是unsafe的方法,來達到併發安全的目的
·當需要在某個位置設定節點的時候,則會通過Synchronized的同步機制來鎖定該位置的節點。
·在陣列擴容的時候,則通過處理的步長和fwd節點來達到併發安全的目的,通過設定hash值為MOVED
·當把某個位置的節點複製到擴張後的table的時候,也通過Synchronized的同步機制來保證現程安全
感謝Ouka傅的原始碼分析,https://www.cnblogs.com/zerotomax/p/8687425.html#go5,我也是從這篇部落格裡學來的