ConcurrentHashMap 精華代碼很多,前面分析了 helpTransfer 和 transfer 和 putVal 方法,今天來分析一下 addCount 方法,該方法會在 putVal 方法中調用。
一起來看看 addCount 是如何操作的。
源碼分析
/** * 變更容器大小 * * @param x 添加的元素個數 * @param check if < 0, 不檢查調整, if <= 1 只有在未競爭時才檢查 */ private final void addCount(long x, int check) { // 計數盒子 CounterCell[] as; long b, s; // 當計數盒子不爲null 或 通過cas直接更新baseCount失敗 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; // 無競爭標記 boolean uncontended = true; // 當計數盒子爲null 或 計數盒子長度爲0 // 或 隨機取計數盒子中的1個元素如果爲null 或 通過CAS直接更新該隨機元素失敗 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(); } if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; // sizeCtl 閾值(僅在數組未初始化時,等於數據容量) // 如果map.size() 大於 sizeCtl(達到擴容閾值需要擴容) // 且table 不是空;且 table 的長度小於 1 << 30。(可以擴容) while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { // 根據 length 得到一個標識 int rs = resizeStamp(n); // 是否正在擴容 if (sc < 0) { // 如果 sc 的低 16 位不等於 標識符(校驗異常 sizeCtl 變化了) // 如果 sc == 標識符 + 1 (擴容結束了,不再有線程進行擴容) //(默認第一個線程設置 sc == rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減 1。 // 這個時候,sc 就等於 rs + 1) // 如果 sc == 標識符 + 65535(幫助線程數已經達到最大) // 如果 nextTable == null(結束擴容了) // 如果 transferIndex <= 0 (轉移狀態變化了) // 結束循環 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 如果可以幫助擴容,那麼將 sc 加 1. 表示多了一個線程在幫助擴容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) // 擴容 transfer(tab, nt); } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) // 更新 sizeCtl 爲負數後,開始擴容。 transfer(tab, null); s = sumCount(); } } }
CounterCell數組的作用是什麼呢?ConcurrentHashMap是採用CounterCell數組來記錄元素個數的,像一般的集合記錄集合大小,直接定義一個size的成員變量即可,當出現改變的時候只要更新這個變量就行。爲什麼ConcurrentHashMap要用這種形式來處理呢? 問題還是處在併發上,ConcurrentHashMap是併發集合,如果用一個成員變量來統計元素個數的話,爲了保證併發情況下共享變量的的安全,勢必會需要通過加鎖或者自旋來實現,如果競爭比較激烈的情況下,size的設置上會出現比較大的衝突反而影響了性能,所以在ConcurrentHashMap採用了分片的方法來記錄大小,具體什麼意思,我們來分析下
public class ConcurrentHashMap<K,V> extends Abstract{ ... // 標識當前cell數組是否在初始化或擴容中的CAS標誌位 private transient volatile int cellsBusy; // counterCells數組,總數值的分值分別存在每個cell中 private transient volatile CounterCell[] counterCells; @sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } } //看到這段代碼就能夠明白了,CounterCell數組的每個元素,都存儲一個元素個數,而實際我們調用size方法就是通過這個循環累加來得到的 //又是一個設計精華,大家可以借鑑; 有了這個前提,再會過去看addCount這個方法,就容易理解一些了 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; } }
總結一下
總結下來看,addCount 方法做了 2 件事情:
-
對 table 的長度加一。無論是通過修改 baseCount,還是通過使用 CounterCell。當 CounterCell 被初始化了,就優先使用他,不再使用 baseCount。
-
檢查是否需要擴容,或者是否正在擴容。如果需要擴容,就調用擴容方法,如果正在擴容,就幫助其擴容。
有幾個要點注意:
-
第一次調用擴容方法前,sizeCtl 的低 16 位是加 2 的,不是加一。所以 sc == rs + 1 的判斷是表示是否完成任務了。因爲完成擴容後,sizeCtl == rs + 1。
-
擴容線程最大數量是 65535,是由於低 16 位的位數限制。
-
這裏也是可以幫助擴容的,類似 helpTransfer 方法。