Java学习--ConcurrentHashMap原理分析
Java学习--ConcurrentHashMap原理分析
本章学习Java中的ConcurrentHashMap(后面简称CHM),了解其并发安全原理,hash碰撞的解决方法,红黑树,size的获取处理方式,以及CHM的并发扩容。本次分析基于jdk1.8。
@
初始化initTable
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//如果table当前为null或者length==0的情况下,开始进行table初始化操作
tab = initTable();
//...
}
}
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//判断table是否需要初始化,如果是进入循环
while ((tab = table) == null || tab.length == 0) {
//判断当前是否有线程正在初始化table,否则让出时间片
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//初始化之前设置SIZECTL标识
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
在putVal中,如果table
当前为null或者length==0的情况下,开始进行table初始化操作。在initTable()
中需要关注SIZECTL
这个成员变量,当其等于-1的情况下则表明当前table
正在初始化,其他线程让出时间片后自旋,直到table
初始化完成后返回。
putVal
第一次赋值
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
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();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//当初始化完成后,开始put值。第一次put时,通过计算得出当前key对应数组下标,取出对应下标node,node为空时,进行node初始化
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//...
}
初始化完成后,重新开始循环并开始准备初始化node
。进入第二判断可以看到,首先通过tabAt
确定该key对应的数组下标位置,因为是第一次put,所以通过下标从table
数组中取到的node
一定为null,因此开始初始化node
。node
的初始化并没有加锁而是通过cas操作完成,只有当其为null的时候才将new Node存入table
中。
第二次赋值
else {
V oldVal = null;
//锁住first节点
synchronized (f) {
//再次判断节点是否匹配
if (tabAt(tab, i) == f) {
//fh为first节点的hash值,大于等于0的情况下表示当前为普通节点,因此直接循环节点并初始化值
if (fh >= 0) {
binCount = 1;
//通过循环来从node链表中找到对应的node并判断如果存在覆盖值,否则初始化
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的next指向当前初始化的node
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//...先忽略红黑树情况put
}
从上面源码分析得到一些信息,在CHM中,锁的颗粒度为firstNode
,可以保证在table
数组为16的情况下支持最大16个线程并发执行。然后当fn
>=0的情况下,循环node
链表,如果循环中发现有重复key的情况下,覆盖对应旧值,否则在链尾插入一个新的node
节点。
红黑树情况赋值
我们知道,在Java1.8中,当HashMap的链表长度超过一定阈值,并且数组长度超过阈值,将会把链表结构转为红黑树。这样做的目的是因为,当单链的长度过长之后,在查询对应链表中key的时候,查询的时间复杂度会提升,查询效率降低。因为红黑树是一种自平衡的二叉查找树
,因此对于查询时间复杂度最大提升为o(logn)。了解了为什么要转为红黑树后,接下来了解一下红黑树的基本概念与性质。
红黑树概念
不了解红黑树的概念的建议先学习一下红黑树,对于后面去分析链表转红黑树、为什么转红黑树等逻辑会有很大帮助。可以参考下文链接。
红黑树性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3.所有叶子都是黑色。(叶子是NUIL节点)
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5.. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
是性质4导致路径上不能有两个连续的红色节点确保了这个结果。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
因为红黑树是一种特化的二叉查找树,所以红黑树上的只读操行与普通二叉查找树相同。
链表转红黑树
红黑树的概念与性质了解完成后,对我们的源码分析会有很大帮助,接下来回到源码,看一下如果是红黑树的put场景。
//当fh小于0的时候,该链表已经转换为了红黑树结构,因此进入红黑树的putVal
else if (f instanceof TreeBin) {
Node<K,V> p;
//这里binCount
binCount = 2;
//开始进行红黑树结构putVal
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
}
if (binCount != 0) {
//判断节点长度如果到达红黑树转换阈值,则将进入node单向链表转为红黑树逻辑
if (binCount >= TREEIFY_THRESHOLD)
//这里实际除了判断转换阈值外,还会判断数组长度来决定是转红黑树还是进行扩容
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//判断table数组长度是否小于64,如果小于则扩容,否则将链表转为红黑树
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<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;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
红黑树的转换和put逻辑并不复杂,复杂的点实际在于红黑树的数据结构处理,插入删除自平衡的处理。理解了红黑树在去看插入、删除的逻辑其实就很容易了。
addCount
put方法执行完成后,开始进行count++了。CHM的count++采用分布式的逻辑,将具体的count数组保存在多个数组中,接下看一下addCount的源码。
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add
* @param check if <0, don't check resize, if <= 1 only check if uncontended
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//判断如果counterCells不等于空、或者cas baseCount失败,则通过CounterCell来记录数量
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//然后就是通过cas的++操作
/*
* 这里的几个判断:
* 1.如果CounterCell[]为空,则直接fullAddCount。
* 2.如果CounterCell[random]为空,则直接fullAddCount。
* 3.通过cas直接替换,如果出现并发则修改失败,然后直接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;
}
if (check <= 1)//如果小于等于1,则表示不需要检查扩容
return;
s = sumCount();
}
//...
}
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//如果probe为0的情况下,表示没有竞争并且没有初始化probe
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) {//类似put方法处拿到CounterCell的下标并获取对应下标下的CounterCell数据
if (cellsBusy == 0) {//0表示没有在进行扩容或者初始化 // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//下面是开始初始化CounterCell了
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
//如果有并发并且probe不等于0
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
//否则通过cas去递增
break;
else if (counterCells != as || n >= NCPU)//扩容的大小不会超过CPU数量
collide = false; // At max size or stale
else if (!collide)
//设置扩容标识。到这一步实际是并发较大的场景,程序判断如果有较大的并发则进行扩容
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//开始扩容时设置扩容标识
try {
//做CounterCell扩容
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);
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//这里开始初始化CounterCell
boolean init = false;
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;
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
扩容
if (check >= 0) {//当check >=0时,表示检查是否需要扩容
Node<K,V>[] tab, nt; int n, sc;
//s:这里s表示当前count数量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//通过这个方法得到一个扩容戳
int rs = resizeStamp(n);
if (sc < 0) {//多线程扩容检查
//这里判断以下5个条件内,只要有一个为true则不能帮助扩容
//(sc >>> RESIZE_STAMP_SHIFT) != rs : -2145714174 >>> 16 = 32795 != 32795 sc右移16位表示将高位转换为低位,sc高位记录扩容戳,低位记录并发扩容线程数
//sc == rs + 1 : 表示扩容结束
//sc == rs + MAX_RESIZERS : 表示支持扩容最大线程数
//(nt = nextTab) == null 表示扩容结束
//transferIndex <= 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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//开始扩容
transfer(tab, null);
s = sumCount();
}
}
在扩容阶段,先检查是否到达扩容临界点。然后开始判断是否扩容或者检查是否可以帮助扩容。是否可以帮助扩容的检查:
- 是否不在同一个扩容戳下
- 是否扩容已经结束
- 是否支持扩容的线程数达到临界点
- 是否已经没有可以领取的扩容节点
以上检查一项为true则表示不能帮助扩容。检查完是否可以扩容后开始进入真正执行扩容的方法。
sc == rs + 1和sc == rs + MAX_RESIZERS两处判断是否可以帮助扩容的方法有点奇怪,看起来是不会成立的两个比较,因为在开始扩容时,会通过cas将sizeCtl修改为(rs << RESIZE_STAMP_SHIFT) + 2,rs扩容戳为正数,以32795为例左移16位等于-2145714176,在+2=-2145714174,因此上面两个判断不会成立。这块问题官方已经有帖子出过说明:
In the above code, condition of (sc == rs + 1 || sc == rs + MAX_RESIZERS ) would never be true , since the value of rs is positive and the value of sc is negative .
The correct condition should be (sc >>> RESIZE_STAMP_SHIFT) == rs + 1 || (sc >>> RESIZE_STAMP_SHIFT) == rs + MAX_RESIZERS, which can be used to dedect if resizing process finished or resizing threads reaches maxmium limitation
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)FREQUENCY : always
transfer
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//这里的stride表示每个线程处理的扩容节点最大范围,这里通过机器CPU来计算得出每个线程分配均匀的处理范围
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
//当nextTab为null的时候表示第一次进入扩容,需要初始化扩容的node数组,默认就是长度翻一倍
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用于标识节点在进行扩容
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;//first节点和对应的hash
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) {//表示扩容完成,将nextTab赋值给table,并重置sizeCtl
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {//sc的记录,高位为扩容戳,低位记录并发扩容线程数,当本次线程扩容处理完成后,线程数-1,如果全部处理完成,则标记finishing为true
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//标记处理完成后,会重新做一次检查,检查完成后进入上面的判断,复制table并重置sizeCtl
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)//如果当前节点槽位为null直接cas一个forwardNode标识已经处理完成
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)//如果fh==-1表示节点正在处理
advance = true; // already processed
else {
synchronized (f) {//锁住节点
if (tabAt(tab, i) == f) {//再次校验f节点
Node<K,V> ln, hn;//ln表示的lowNode、hn表示的highNode。高位节点和低位节点
if (fh >= 0) {//再次校验fh
int runBit = fh & n;
Node<K,V> lastRun = f;
//循环节点链,记录最后区分的节点为lastRun,并记录其runBit
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) {//为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);
}
//将分类好的高低位节链set进新的tab中,低位保持下标不便,高位进行迁移
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;
}
}
//红黑树的逻辑同链表类似,因为高低位拆分后,红黑树的长度可能不满足红黑树要求,因此会判断是否需要转为链表
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;
}
}
}
}
}
}
扩容的方法,首先通过机器的CPU数量来计算出合理的扩容分配节点槽位范围,然后通过cas transferIndex来领取任务。任务领取后,从后向前遍历处理每个节点槽位,节点槽位处理过程使用了高低链的算法来判断节点槽位是否需要变化,最后处理完成后设置处理完成标记forwardNode。当全部的槽位处理完成后,会重新检查一遍,最后将nextTab赋值给table后,扩容完成。
为什么要做高低位的划分
要想了解这么设计的目的,我们需要从 ConcurrentHashMap 的根据下标获取对象的算法来看,在 putVal 方法中 1018 行
(f = tabAt(tab, i = (n - 1) & hash)) == null
通过(n-1) & hash 来获得在 table 中的数组下标来获取节点数据,【&运算是二进制运算符,1
& 1=1,其他都为 0】假设我们的 table 长度是 16, 二进制是【0001 0000】,减一以后的二进制是 【0000 1111】
假如某个 key 的 hash 值=9,对应的二进制是【0000 1001】,那么按照(n-1) & hash 的算法
0000 1111 & 0000 1001 =0000 1001 , 运算结果是 9
当我们扩容以后,16 变成了 32,那么(n-1)的二进制是 【0001 1111】
仍然以 hash 值=9 的二进制计算为例
0001 1111 & 0000 1001 =0000 1001 ,运算结果仍然是 9我们换一个数字,假如某个 key 的 hash 值是 20,对应的二进制是【0001 0100】,仍然按照(n-1) & hash
算法,分别在 16 为长度和 32 位长度下的计算结果
16 位: 0000 1111 & 0001 0100=0000 0100
32 位: 0001 1111 & 0001 0100 =0001 0100
从结果来看,同样一个 hash 值,在扩容前和扩容之后,得到的下标位置是不一样的,这种情况当然是
不允许出现的,所以在扩容的时候就需要考虑,
而使用高低位的迁移方式,就是解决这个问题.
大家可以看到,16 位的结果到 32 位的结果,正好增加了 16.
比如 20 & 15=4 、20 & 31=20 ; 4-20 =16
比如 60 & 15=12 、60 & 31=28; 12-28=16
所以对于高位,直接增加扩容的长度,当下次 hash 获取数组位置的时候,可以直接定位到对应的位置。
这个地方又是一个很巧妙的设计,直接通过高低位分类以后,就使得不需要在每次扩容的时候来重新计
算 hash,极大提升了效率。摘要自咕泡学院教材
size
public int size() {
//拿到count
long n = sumCount();
//判断count范围
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
//遍历CounterCell数组,获得每个CountCell记录的count数量,
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;
}
size的方法逻辑相对要简单很多了。
总结
CHM中数据的存储采用node数组+链表+红黑树的结构,每次用户操作,锁的颗粒度在于其中一个节点槽位上,因此可以大大提升并发效率。红黑树的设计是考虑当链表过长的时候,对链表的遍历效率降低,因此会在链表到达阈值的时候将链表转为红黑树、反之如果没有到达阈值,会将红黑树在转换为链表。
CHM中采用将数量分布在不同的CountCell中来存储以提升addCount的效率。
CHM扩容:支持并发扩容,在扩容阶段通过领取任务的方式来领取需要处理的槽位,每个线程领取的任务数量通过CPU核心时动态计算出最合理的范围。扩容阶段会生成一个扩容戳标识扩容的阶段,通过将扩容戳 << 16后并 +2来记录,高位为扩容戳,低位为扩容线程数。用于后面并发扩容结束判断。对于扩容的数据迁移,通过高低链的方式来重新构建链表,这样做的好处是扩容后通过hash值来获取下标的算法实际值会有所变化,因此将会变化的拆分出来高位链,而不会变化的为低位,然后将低位链保存下标不变,高位链修改下标位置,就使得不需要在每次扩容的时候来重新计算 hash,极大提升了效率。