引言:
首先“打个广告”,
对于sizeCtl变量,如果读者还认为取值-N 时代表有N-1个扩容线程(因为基本资料都是这么说的),
建议先阅读我的另一篇文章:
ConcurrentHashMap的sizeCtl含义纠正
引言:
看了很多资料,有说的好的,但感觉有些地方讲得不是很好,所以加入了自己的理解,写出了这篇博文,希望对各位有一点帮助。
精华分析
因为源码跟注释都很多,我先直接把精华的分析总结出来。
- transfer是支持并发扩容的,而且每个线程处理的桶区间是不相交的

- for死循环确保整个扩容任务完成
- while (advance)循环中,负责分配各线程的处理桶区间,扩容时逆序扩容,即从高位到低位,而不像HashMap顺序扩容
- 对链表的处理中,使用的是新建结点的方式,而不修改原结点顺序,保证了在扩容期间,get操作也能正常使用且能返回正确结果
- 链表的拆分获得是一条逆序链表,而另一条链表顺序不定,而不是逆序/顺序链表 (当然指的是能拆分的一般情况)
- 处理完成需要放置ForwardingNode,作为占位符,表示已转移该字符
transfer()源码解析
ps:注释中所说关于扩容线程的增加、减少,并不是真的指数值增加/减少了,只是便于直观理解。
private final void transfer(ConcurrentHashMap.Node<K, V>[] tab, ConcurrentHashMap.Node<K, V>[] nextTab) {
int n = tab.length, stride;
// 根据CPU核心数,分配每个CPU处理的桶数量。
// 目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,
// 如果桶较少的话,默认一个 CPU(即一个线程)处理 16 个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
//nextTab未初始化,说明这是第一个扩容线程
if (nextTab == null) { // initiating
try {
//扩容两倍
@SuppressWarnings("unchecked")
ConcurrentHashMap.Node<K, V>[] nt = (ConcurrentHashMap.Node<K, V>[]) new ConcurrentHashMap.Node<?, ?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
//扩容时n<<1造成溢出,证明当前已无法扩容为2倍,直接设置为最大值
sizeCtl = Integer.MAX_VALUE;
return;
}
//更新全局变量
nextTable = nextTab;
//初始值为n,原数组长度
transferIndex = n;
}
int nextn = nextTab.length;
//创建一个fwd结点,用于标记数组元素已被转移
ConcurrentHashMap.ForwardingNode<K, V> fwd = new ConcurrentHashMap.ForwardingNode<K, V>(nextTab);
//advance表示是否需要推进,标记当前桶是否已处理完毕
// true:需要再次推进一个下标(i--),
// false:不能推进下标,需要先处理当前的下标,才能继续推进
boolean advance = true;
//标记整个扩容过程是否已完成
boolean finishing = false;
//死循环,保证修改成功
//i : 当前处理的桶下标 ; bound :当前线程可处理的桶边界
for (int i = 0, bound = 0; ; ) {
ConcurrentHashMap.Node<K, V> f;
int fh;
// 该循环用于控制 i 递减。此外,每个线程都会通过该循环取得自己需要转移的桶的区间
// 之所以需要用循环控制,笔者认为主要是第三个if的CAS操作有失败的可能性
while (advance) {
int nextIndex, nextBound;
// 对 i 减一,判断是否大于等于 bound
// 1. 条件不成立,说明该线程当前已处理完成分配到的桶
// 2. 否则说明线程任务未完成,设置为false,以便跳出循环,开始处理当前桶
// 若finishing为true,即整个扩容已结束,自然无需推进
if (--i >= bound || finishing)
advance = false;
// 两个目的: 1. 更新nextIndex,因为transferIndex是volatile的,保证获取最新的转移下标
// 若满足if,说明总扩容任务已经完成,设置i=-1,是为了跳出循环后,满足if (i < 0)
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// CAS修改transferIndex。 进入该if的情况
//1.第一次进入循环,分配每个线程处理的桶的区间
//2. 不满足上面两个情况,说明当前线程任务已完成,但总扩容任务还没完成,再次分配扩容任务
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//更新桶边界以及当前桶下标
bound = nextBound;
// 注意,从i--以及这里的赋值,可以得知扩容是从高位到低位,即逆序处理,而HashMap中是顺序处理的
i = nextIndex - 1;
// 任务分配好了,跳出循环处理当前i
advance = false;
}
}
// 笔者认为这里只有 i<0 的条件是有意义的,其余两个都无意义,
// 因为i能被赋值的最大值是(nextIndex - 1)=(transferIndex -1)<=(n -1 ),因为transferIndex初始值为n,而且transferIndex与i都是只减不增,不可能达到其他两个条件
// i<0,只对应上方while循环中的 i=-1 ,不会对应第一个if中的 --i>=bound
// 因为bound = nextBound >=0 ,所以要满足if (--i >= bound),--i>=0必须成立,所以 i<0必不可能成立。而若不满足 1、2个if,就会如上文注释所说再次获取扩容任务
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//总扩容任务完成
if (finishing) {
//置null, 保持nextTable只会在扩容时不为null的特性
nextTable = null;
//更新全局变量
table = nextTab;
//结果:1.5*n ,但别忘了n是旧数组长度,新数组长度为2n,而1.5n = 0.75 * 2n , 所以还是等于数组长度的0.75倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//总扩容任务未完成,但当前线程已完成,CAS减少扩容线程数量
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//这里看似迷的操作,其实是一一对应回addCount / TryPresize中把sizeCtl置为负值时的操作: U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//能到这个逻辑时,说明没有其他扩容线程了,即当前线程是最后的扩容线程
//总扩容已完成,更新finishing,advance设为true是为了能进入上述的while循环,而且不会有影响,因为一定满足if(finnishing) ,会直接跳出循环
finishing = advance = true;
//稳如老狗 ,重新检查一遍全部桶
//注意:这里不会受到当前线程的bound影响,执行代码顺序就是
//1. while (advance) if (--i >= bound || finishing) ,借助这里完成i递减,然后便跳出循环 (妙,膜拜)
//2. 下方的if ((fh = f.hash) == MOVED) advance = true;
//3. 1、2循环,至 i<0, 进行本if (i < 0) {if (finishing) } ,至此结束。这里也只是对i<0起作用,跟上面说"另两个if无意义"并不冲突
i = n;
}
// 若当前桶为空,直接放一个fwd占位,表示已转移
} else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//已经是fwd了,说明已经被处理过了,继续推进
//拓:有文章说这里表示有别的线程进行过了,但笔者认为这个说法不严谨,因为不一定是别的线程,也有可能是当前线程处理的那部分结点,当然不影响理解,只是提一下
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 当前桶还没处理,加锁进行处理
synchronized (f) {
// 再次确认当前 i 位置,确保没被其他线程修改
if (tabAt(tab, i) == f) {
//ln、hn,按笔者理解,其实就是lowNode 跟 highNode 缩写
ConcurrentHashMap.Node<K, V> ln, hn;
// hash>=0 说明是链表 , TreeBin hash = -2 , 剩下那个-3的结点不会在这里起效
if (fh >= 0) {
// 这里的逻辑跟HashMap的扩容一样,不了解的可以看我另一篇博文:https://blog.csdn.net/Unknownfuture/article/details/105181447
int runBit = fh & n;
//注意这里的实现
ConcurrentHashMap.Node<K, V> lastRun = f;
for (ConcurrentHashMap.Node<K, V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
//runBit跟lastRun 只会在 runBit发生变化时进行更改
// 这样在runBit都是一样的情况下,只需要指向lastRun就可以结束了,而不需要再连接上lastRun后面那些runBit一样的结点
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//runBit为0,说明最后更新的runBit=0,同时lastRun也应该继续保留在当前位置
if (runBit == 0) {
//更新ln、hn,注意hn为null
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
//结束条件是 p != lastRun ,而不是p != null ,原因如上,节省了循环时间
for (ConcurrentHashMap.Node<K, V> p = f; p != lastRun; p = p.next) {
int ph = p.hash;
K pk = p.key;
V pv = p.val;
// 为0,留在本位置,否则应放到高位
// 注:循环结束后,会分为两条链表(ln、hn分别为两条链表头结点)
// 一条为逆序链表 (顺序指的是按runBit分类,在原数组的位置)
// 另一条链表顺序不定,而不是有的资料所说的顺序/逆序链表
// 而为逆序链表的那条,就是在循环前为null的那条,比如若hn=null, 那hn就是逆序链表
// 原因可以自己画一画,或者看笔者的图
// 此外,注意这里的实现是复制新的结点,而不是像hashMap中那样直接另指针指向原结点
// 笔者的理解:不修改原结点顺序,保证了在扩容期间,get操作也能正常使用且能返回正确结果
if ((ph & n) == 0)
ln = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, ln);
else
hn = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, hn);
}
//ln放到原位置,hn放到扩容后的位置 +n ,不多解释
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//fwd占位
setTabAt(tab, i, fwd);
//标记当前桶处理完了,可以往前推进
advance = true;
}
// 红黑树类型,逻辑差不多,笔者也不是很懂红黑树,就不班门弄斧了
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.TreeBin<K, V> t = (ConcurrentHashMap.TreeBin<K, V>) f;
ConcurrentHashMap.TreeNode<K, V> lo = null, loTail = null;
ConcurrentHashMap.TreeNode<K, V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (ConcurrentHashMap.Node<K, V> e = t.first; e != null; e = e.next) {
int h = e.hash;
ConcurrentHashMap.TreeNode<K, V> p = new ConcurrentHashMap.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 ConcurrentHashMap.TreeBin<K, V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new ConcurrentHashMap.TreeBin<K, V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
附图:对链表的处理
蓝色节点表示runBit为0,红色节点为1
假设某个桶链表结点如下:

此时 ln=null , hn = lastRun , for循环拆分链表后:
- ln链表 ,变为逆序

- hn链表

此处举的是特例,刚好是顺序的。
但若2结点也是红色,那结果是 4->2->6->7->8 ,
显然既不是顺序也不是逆序。
本文完,若有误,欢迎指出。
参考资料:
并发编程——ConcurrentHashMap#transfer() 扩容逐行分析
深入分析ConcurrentHashMap1.8的扩容实现

1149

被折叠的 条评论
为什么被折叠?



