JDK1.8的ConcurrentHashmap
利用 ==CAS + synchronized== 來保證併發更新的安全
底層使用==數組+鏈表+紅黑樹==來實現
---------------------------------------------------------------------------------------
線程不安全的HashMap
因爲多線程環境下,使用Hashmap進行put操作會引起死循環,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。
效率低下的HashTable容器
HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因爲當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
ConcurrentHashMap的鎖分段技術
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因爲所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。
---------------------------------------------------------------------------------
Java的ConcurrentHashMap簡介
簡介
ConcurrentHashMap是Java中的一個線程安全且高效的HashMap實現。平時涉及高併發如果要用map結構,那第一時間想到的就是它。
那麼我就這幾個方面瞭解一下ConcurrentHashMap:
1)ConcurrentHashMap在JDK8裏結構
2)ConcurrentHashMap的put方法、szie方法等
3)ConcurrentHashMap的擴容
4)HashMap、Hashtable、ConccurentHashMap三者的區別
5)ConcurrentHashMap在JDK7和JDK8的區別
源碼分析
ConcurrentHashMap在JDK8裏結構
首先來看下底層的組成結構:
其實和 1.8 HashMap 結構類似,當鏈表節點數超過指定閾值的話,也是會轉換成紅黑樹的,大體結構也是一樣的。
那麼它到底是如何實現線程安全的?
答案:其中拋棄了原有的Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。至於如何實現,那我繼續看一下put方法邏輯
put方法的邏輯
ConcurrentHashMap最常用的方法也就是put方法和get方法,那麼下面主要看代碼註釋,便於理解。
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1. 計算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2. 如果當前table還沒有初始化先調用initTable方法將tab進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//3. tab中索引爲i的位置的元素爲null,則直接使用CAS將值插入即可
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//4. 當前正在擴容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//5. 當前爲鏈表,在鏈表中插入新的鍵值對
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) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 6.當前爲紅黑樹,將新的鍵值對插入到紅黑樹中
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;
}
}
}
}
// 7.插入完鍵值對後再根據實際大小看是否需要轉換成紅黑樹
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//8.對當前容量大小進行檢查,如果超過了臨界值(實際大小*加載因子)就需要擴容
addCount(1L, binCount);
return null;
}
請先看完代碼註釋,有個大致的瞭解,然後我們更加詳細的學習一下:
這個put的過程很清晰,對當前的table進行無條件自循環直到put成功,可以分成以下六步流程來概述:
1、判斷Node[]數組是否初始化,沒有則進行初始化操作
2、通過hash定位數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭節點),添加失敗則進入下次循環。
3、檢查到內部正在擴容,就幫助它一塊擴容。
4、如果f!=null,則使用synchronized鎖住f元素(鏈表/紅黑樹的頭元素)。如果是Node(鏈表結構)則執行鏈表的添加操作;如果是TreeNode(樹型結構)則執行樹添加操作。
5、判斷鏈表長度已經達到臨界值8(默認值),當節點超過這個值就需要把鏈表轉換爲樹結構。
6、如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容
1.spread(key,hashCode())
這方法作用重哈希,以減小Hash衝突
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
該方法主要是將key的hashCode的低16位於高16位進行異或運算,這樣不僅能夠使得hash值能夠分散能夠均勻減小hash衝突的概率,另外只用到了異或運算,在性能開銷上也能兼顧。
2.initTable方法
主要作用將tab進行初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// 1. 保證只有一個線程正在進行初始化操作
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 2. 得出數組的大小
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 3. 這裏才真正的初始化數組
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 4. 計算數組中可用的大小:實際大小n*0.75(加載因子)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
爲了保證能夠正確初始化,在第1步中會先通過if進行判斷,若當前已經有一個線程正在初始化即sizeCtl值變爲-1,這個時候其他線程在If判斷爲true從而調用Thread.yield()讓出CPU時間片。正在進行初始化的線程會調用U.compareAndSwapInt方法將sizeCtl改爲-1即正在初始化的狀態。另外還需要注意的事情是,在第四步中會進一步計算數組中可用的大小即爲數組實際大小n乘以加載因子0.75.可以看看這裏乘以0.75是怎麼算的,0.75爲四分之三,這裏n - (n >>> 2)是不是剛好是n-(1/4)n=(3/4)n,挺有意思的吧:)。如果選擇是無參的構造器的話,這裏在new Node數組的時候會使用默認大小爲DEFAULT_CAPACITY(16),然後乘以加載因子0.75爲12,也就是說數組的可用大小爲12。
3.CAS關鍵操作
tabAt()該方法用來獲取table數組中索引爲i的Node元素。
casTabAt()利用CAS操作設置table數組中索引爲i的元素
setTabAt()該方法用來設置table數組中索引爲i的元素
4.ConcurrentHashMap的擴容
通過判斷該節點的hash值是不是等於-1(MOVED),代碼爲(fh = f.hash) == MOVED,說明 Map 正在擴容。那麼就幫助 Map 進行擴容。以加快速度。
如何幫助擴容呢?那要看看 helpTransfer 方法的實現。
/**
* 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;
// 如果 table 不是空 且 node 節點是轉移類型,數據檢驗
// 且 node 節點的 nextTable(新 table) 不是空,同樣也是數據校驗
// 嘗試幫助擴容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 根據 length 得到一個標識符號
int rs = resizeStamp(tab.length);
// 如果 nextTab 沒有被併發修改 且 tab 也沒有被併發修改
// 且 sizeCtl < 0 (說明還在擴容)
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 如果 sizeCtl 無符號右移 16 不等於 rs ( sc前 16 位如果不等於標識符,則標識符變化了)
// 或者 sizeCtl == rs + 1 (擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1)
// 或者 sizeCtl == rs + 65535 (如果達到最大幫助線程的數量,即 65535)
// 或者轉移下標正在調整 (擴容結束)
// 結束循環,返回 table
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 如果以上都不是, 將 sizeCtl + 1, (表示增加了一個線程幫助其擴容)
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 進行轉移
transfer(tab, nextTab);
// 結束循環
break;
}
}
return nextTab;
}
return table;
}
基本邏輯已在代碼註釋中,這裏關鍵transfer(),那麼我們繼續深入瞭解一下
private final void transfer(Node<K,V>[] tab, 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,V>[] nt = (Node<K,V>[])new Node<?,?>[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, SIZECTL, 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, i, null, 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,V>(ph, pk, pv, hn);
}
// 在nextTable i 位置處插上鍊表
setTabAt(nextTab, i, ln);
// 在nextTable i + n 位置處插上鍊表
setTabAt(nextTab, i + n, hn);
// 在table i 位置處插上ForwardingNode 表示該節點已經處理過了
setTabAt(tab, i, 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, 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;
}
}
}
}
}
}
擴容過程有點複雜,可以查看上面註釋。這裏主要涉及到多線程併發擴容,ForwardingNode的作用就是支持擴容操作,將已處理的節點和空節點置爲ForwardingNode,併發處理時多個線程經過ForwardingNode就表示已經遍歷了,就往後遍歷,下圖是多線程合作擴容的過程:
5.treeifyBin()鏈表轉紅黑樹的過程
基本邏輯都在代碼註釋中。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//如果整個table的數量小於64,就擴容至原來的一倍,不轉紅黑樹了
//因爲這個閾值擴容可以減少hash衝突,不必要去轉紅黑樹
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
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;
for (Node<K,V> e = b; e != null; e = e.next) {
//封裝成TreeNode
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//通過TreeBin對象對TreeNode轉換成紅黑樹
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
addCount()方法計算ConcurrentHashMap的size
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//更新baseCount,table的數量,counterCells表示元素個數的變化
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//如果多個線程都在執行,則CAS失敗,執行fullAddCount,全部加入count
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//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, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//當前線程發起庫哦哦讓操作,nextTable=null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
put的流程現在已經分析完了,你可以從中發現,他在併發處理中使用的是樂觀鎖,當有衝突的時候才進行併發處理,而且流程步驟很清晰,但是細節設計的很複雜,畢竟多線程的場景也複雜.
get方法
concurrentHashMap的get操作的流程很簡單,可以分爲三個步驟來描述:
1.計算hash值,定位到該table索引位置,如果是首節點符合就返回。
2.如果遇到擴容的時候,會調用標誌正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回。
3.以上都不符合的話,就往下遍歷節點,匹配就返回,否則最後就返回null
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //計算兩次hash
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {//讀取首節點的Node元素
if ((eh = e.hash) == h) { //如果該節點就是首節點就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值爲負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來
//查找,查找到就返回
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首節點也不是ForwardingNode,那就往下遍歷
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
szie方法的邏輯
對於size的計算,在擴容和addCount()方法就已經有處理了,可以注意一下Put函數,裏面就有addCount()函數,早就計算好的,然後你size的時候直接給你.
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a; //變化的數量
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
HashMap、Hashtable、ConccurentHashMap三者的區別
HashMap線程不安全,數組+鏈表+紅黑樹
Hashtable線程安全,鎖住整個對象,數組+鏈表
ConccurentHashMap線程安全,CAS+同步鎖,數組+鏈表+紅黑樹
HashMap的key,value均可爲null,其他兩個不行。
在JDK1.7和JDK1.8中的區別
在JDK1.8主要設計上的改進有以下幾點:
1、不採用segment而採用node,鎖住node來實現減小鎖粒度。
2、設計了MOVED狀態 當resize的中過程中 線程2還在put數據,線程2會幫助resize。
3、使用3個CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。
4、sizeCtl的不同值來代表不同含義,起到了控制的作用。
採用synchronized而不是ReentrantLock
參考文章
https://www.cnblogs.com/aspirant/p/8623864.html
https://juejin.im/post/5aeeaba8f265da0b9d781d16