java8 ConcurrentHashMap源碼學習
ConcurrentHashMap
ConcurrentHashMap是一個HashMap的升級版,是線程安全的,想要了解ConcurrentHashMap就必須得要去了解他的put、get、擴容方法
這裏必須說一下的是, 1.8的ConcurrentHashMap不是使用segment進行併發操作了, 現在太多誤導人的博客了… 雖然源碼中有segment但是整個put, get, 擴容的環節都與segment無關, 並沒有使用segment進行併發控制
put
public V put(K key, V value) {
return putVal(key, value, false);
}
可以看見這裏調用了putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
// 這裏binCount是用於標記當前鏈表的長度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// tab如果是空或者長度爲0就進行初始化
// 在initTable中用cas替換sizeCtl爲-1保證了table在多線程下只會被一個線程初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// tabAt方法中使用了本地方法getObjectVolatile,直接從內存中獲取數組指定位置的值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 該位置值爲空則new一個結點使用cas替換, 這裏cas保證了多線程下的原子性
// 若多個線程進入這一語句塊, cas先比較table[i]的值是否爲null, 若是則替換
// 所以只有一個線程的cas操作可以成功, 其他都失敗
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// MOVED代表着當前位置有線程在進行擴容遷移, 該線程會加入遷移過程
// helpTransfer與transer操作幾乎一樣不多贅述
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// table已經被初始化過了, 當前節點不爲空, 並且擴容沒有在進行
else {
V oldVal = null;
// 將f上鎖
synchronized (f) {
// 再次進行判斷保證i結點還是之前的i結點
if (tabAt(tab, i) == f) {
// fh>=0說明這個f是鏈表, 若f是treebin, 在treebin中是沒有hash這個變量的
if (fh >= 0) {
binCount = 1;
// 遍歷f, 看看key是否已經存在
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;
// key不存在, 新增一個結點
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 當前結點是紅黑樹, 直接調用putTreeVal
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;
}
}
}
}
// binCount不爲0說明新增了結點
if (binCount != 0) {
// binCount大於等於8, 調用treeifyBin
// 在treeifyBin中如果table容量小於64, 則會進行擴容而不是轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 調用addCount, 將baseCount+1或者放入counterCells中
addCount(1L, binCount);
return null;
}
addCount
這個函數的功能就是將baseCount+x或者將x放入counterCells
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 首先判斷計數器是否爲空, 或者讓s=b+x與baseCount進行cas交換, 若失敗則進入語句塊
// 這裏失敗了就會放棄累加baseCount轉而將x存入計數盒子
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// 這裏有三個判斷
// 1.計數器如果爲空則直接調用fullAddCount進行計數器初始化, 上一個if中cas失敗的場景
// 2.取一個隨機數同數組-1進行與運算, 就是取餘獲取下標, 爲空則調用fullAddCount
// 3.到了這一步即代表當前下標計數器不爲空, 所以取當前值與x相加, cas交換
// 如果第三步也失敗了, 那就調用fullAddCount並且uncontended是false
// fullAddCount就是一個計數的函數, 後面會講到
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;
}
// 傳入的binCount <= 1 就直接return不進行擴容檢測
if (check <= 1)
return;
// s是當前table中數據的總數, 這裏包括了baseCount和counterCells中的數
// 由於這個sumCount()的實現非常簡單, 這裏就不贅述了
s = sumCount();
}
// 傳入的binCount >= 0就進行擴容檢測, 這裏可以發現put過來的只要到了這一步一定會檢測
// 而addCount還有被其他方法調用, 所以這裏需要做一個check判斷
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 這裏的s就是上面的sumCount計算出來的數據
// s要大於需要擴容的長度, 基本上是table.length * 0.75
// 若容量已經比2^31還要大就無法擴容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// RESIZE_STAMP_BITS默認爲16
// 這裏的rs用於標記, 是根據n和RESIZE_STAMP_BITS生成的一個高十六位負數
int rs = resizeStamp(n);
// sc正常來說是大於0的, 小於0的情況就是有線程在進行擴容, 那麼這個線程就加入幫助
if (sc < 0) {
// 這裏判斷擴容是否已經完成
// 1.sc右移16位查看標誌位是否相等
// 2.sc如果等於rs+1說明擴容任務完成
// 3.幫助擴容的線程達到了了最大值
// 4.擴容完成的另一種判斷 nextTable是空
// 5.transferIndex小於等於0也說明擴容任務完成
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 當前線程加入擴容任務, sc++
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 當前需要擴容, 但是沒有線程正在進行擴容任務, 就讓當前線程開啓擴容任務
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount
這一段挺難的, 牽扯到了很多種情況
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 獲取一個隨機值
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 當前的計數盒子中這個下標爲空, 表示可以存放一個新的數
if ((a = as[(n - 1) & h]) == null) {
// 判斷這個計數器是否被上鎖, 這裏用上鎖的概念, 其實並不是真的上鎖了
// 因爲cas操作修改了cellsBusy, 保證只有一個線程執行這一個語句塊
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 一個創建標誌
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
// 創建一個新的結點賦值給計數器, 最後finally將cellsBusy賦值爲0
// 這一段看上去很像ReentrantLock的上鎖解鎖過程
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
// 如果創建成功直接break;
if (created)
break;
// 當前計數器不爲空, 後面進入的線程獲取了鎖
// 但是這個數值已經被前一個線程縮修改了
continue; // Slot is now non-empty
}
}
collide = false;
}
// cas操作已經失敗過, 這個變量是addCount傳入的, 表示對計數器的累加操作失敗
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 與addCount中的cas操作一樣, 給計數器累加值, 成功就跳出, 失敗就往下
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// 如果有其他線程創建了新的counterCells或者counterCells的容量大於cpu核心數
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// collide是擴容標誌, 如果不允許擴容就會一直在上一步停留, 到了這一步就會允許擴容
// 然後在下一次循環中直接進入擴容步驟
else if (!collide)
collide = true;
// 擴容步驟, cas獲取鎖, 擴容完畢後, 將擴容標誌改爲false並重新取一個隨機數
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
// 由於if中判斷的counterCells是空, 所以需要初始化計數盒子, cas獲取鎖
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 初始化成功標誌
boolean init = false;
// 初始容量是2, 然後將x放入這個計數盒子
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 計數盒子爲空且cellsBusy是1就給baseCount進行cas加操作
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
transfer
這個方法是擴容的主要方法, 很長
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 這裏的stride用於給線程分配任務, 這裏有n個位置需要進行遷移
// 即一個線程需要處理的是transferIndex - stride ~ transferIndex個位置
// 這裏根據cpu核心數和n來制定, stride最小爲16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 初始化, 傳入的nextTab如果是空, 說明需要初始化, 新數組比舊數組的容量大一倍
// 這裏的初始化由外圍調用的方法保證只被初始化一次
if (nextTab == null) { // initiating
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就是上面說的線程分配任務的標誌位
// 任務分配是數組從後往前分配的
transferIndex = n;
}
int nextn = nextTab.length;
// 這裏的ForwardingNode的hash值就是前面提到的MOVED, 只要一個結點是ForwardingNode
// 那麼其他線程處理到這個結點的時候可以直接跳過
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance是一個標誌位, 表示遷移能否進行
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// 從後往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// advance爲true表示可以進行遷移
// 這裏的i可以理解爲是指向transferIndex的, 而bound指向transferIndex - stride
while (advance) {
int nextIndex, nextBound;
// 這裏就是判斷遷移工作是否已經被分配
// 前面說過一個線程完成stride個位置, i如果等於bound說明已經完成了stride個任務
if (--i >= bound || finishing)
advance = false;
// 這裏是判斷所有的遷移工作是否分配完畢, 因爲transferIndex是從後往前的
// 所以如果transferIndex<=0那麼就說明所有遷移任務都分配完了了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 當前線程獲取這個stride的任務, 前面判斷當前的遷移工作並未被分配
// 所以將transferIndex - stride, 告訴後面線程這個工作我承包了
// 如果transferIndex <= stride就直接將transferIndex 變爲0
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 經過了while循環獲取了工作的線程可以開始遷移工作
// 這裏三個判斷條件都是判斷這個i是否符合遷移條件
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// finishing爲true表示所有工作已經完成
// 這裏進行賦值處理
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 這裏用cas對sc-1表示當前線程完成了自己的工作, addCount中有說到sc的作用
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
}
}
// 如果f結點是空, 那麼用cas替換將tab[i]標誌爲MOVED
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 這裏表示這個節點已經被處理過
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 開始加鎖處理遷移
synchronized (f) {
if (tabAt(tab, i) == f) {
// 接下來的操作跟HashMap中的resize操作極其相似
// 都是將當前的鏈表一分爲二, 一部分放在當前位置i, 一部分放在i+n
// 置於爲什麼可以這樣操作在最後會講
Node<K,V> ln, hn;
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);
}
// cas操作替換值
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;
}
}
}
}
}
}
這裏講一下鏈表可以一分爲二
假設n是16將要擴容爲32
因爲16是10000, 存入table中是同1111進行與運算的, 而存入新的table中是同11111進行與運算的
新數組中i存放的數據與i+n存放的數據唯一不同在於第五位二進制數是不是1
如果是1那麼就是存放在i+n的 如果是0就是存放在i中的
所以爲了判斷第五位是不是1, 就可以同16進行與運算如果第五位是0, 那麼他們與運算得出的結果就是0
所以可以把這個結點放入lo中, 而結果不爲0的就可以存放到i+n中
這個結果跟利用hash值重新同32-1進行與運算得出的結果一致
後記
寫完這一篇學習記錄之後, 更加清楚了ConcurrentHashMap中這幾個方法的用處, 閱讀源碼真的提升很大, ConcurrentHashMap中的併發設計十分精妙
而這一篇學習記錄也僅僅是將put的流程講述了一遍
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
public native Object getObjectVolatile(Object var1, long var2);
通過以上代碼可以看見, tabAt就是使用Unsafe類中的本地方法, 直接從內存中獲取這個對象, 使用的是tab的內存地址+索引的偏移量去讀取, 可是table和Node中的next在ConcurrentHashMap中本身就是用volatile關鍵字修飾的, 直接讀取也是保證可見性的
所以這個問題無法從這個出發點去解決, 後來閱讀了ArrayList的源碼之後發現, 數組類在jvm中會自動檢測數組的越界問題
對於數組類型,每一維度將使用一個前置的“[”字符來描述,如一個定義爲“java.lang.String[][]”類型 的二維數組將被記錄成“[[Ljava/lang/String;”,一個整型數組“int[]”將被記錄成“[I”。
如果C是一個數組類型,並且數組的元素類型爲對象,也就是N的描述符會是類 似“[Ljava/lang/Integer”的形式,那將會按照第一點的規則加載數組元素類型。如果N的描述符如前面所 假設的形式,需要加載的元素類型就是“java.lang.Integer”,接着由虛擬機生成一個代表該數組維度和元 素的數組對象。
上面是深入理解java虛擬機一書中與數組類型有關的內容, 第二段是類加載的解析階段中所描述的, 所以Node<K, V>[]會先被包裝成一個數組類, 所以如果在數組中獲取下標爲-1的元素會直接拋出異常
這可能會帶來一些性能的消耗, 所以這裏使用Unsafe直接去操作內存讀取可能是出於性能方面的優化考量