4 transfer()-扩容
在size大小大于阈值(容量的3/4)时,会进行扩容;当某个操作检测到结点hash值为MOVED时,会帮忙扩容,间接调用 transfer();tryPresize()也会调用();源代码如下:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
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];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
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;
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) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
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
}
}
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) {
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);
}
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;
}
}
}
}
}
}
多线程完成数据迁移主要模型如下图所示:
- 多线程迁移数据,为提高效率,采用分段的形式,来完成。
- 数据迁移由后向前执行
- 如果多个线程在同一范围内相遇,产生冲突,通过syncronized来实现并发访问控制控制
- 数据迁移,重新hash计算等于0分到新nextTab的索引i处;不等于0分到i+n处;
- 红黑树拆分的元素个数如果小于等于阈值6,那么红黑树就重新转换为单链表。
详细执行流程步骤如下:
-
计算tab程度n,计算范围长度stride
-
判断如果传参nextTab为null,说明是首个调整大小的线程
- 初始化新的hash数组,大小为原数组大小的2倍
- transferIndex置为n,该变量表示整个hash数组下一个要迁移范围的起始索引+1
-
以nextTab新建ForwardingNode结点fwd
-
设置advance用于确定线程要迁移的索引值;finishing用于确认是否迁移完毕
-
for循环,i初始0,bound(边界)初始0
-
while判断advance是否为true
-
nextIndex表示当前线程要下一个迁移数据范围的起始索引;nextBound表示当前线程下一个要迁移数据范围的结束边界;
-
判断–i是否大于等于bound或者finishing已经结束
- advance置为false
-
否则判断(nextIndex=transferIndex)是否小于等于0
- i置为-1;advance置为false
-
否则cas设置transferIndex值由nextIndex变为新计算nextBound
- 当前要迁移范围边界设置为nextBound
- i置为nextIndex-1
- advance置为false
-
-
确定迁移范围起始索引i后,判断如果i小于0或者i大于等于n或者i+n大于等于nextn新数组长度即2n
-
表示当前线程迁移已经执行到数组头,有其他线程迁移工作还未完成或者全部完成。
-
判断是否finishing
- sizeCtl置为新容量的3/4,返回
-
判断cas把sizeCtl-1是否成功
- 判断sc-2是否等于最初设置rs(戳左移16位),即判断是否不是最后一个迁移线程
- 不是最后一个迁移线程,直接return
- 是最后一个迁移线程,finishing,advanc置为true,i置为n
- 判断sc-2是否等于最初设置rs(戳左移16位),即判断是否不是最后一个迁移线程
-
-
否则判断索引i处的结点f是否为null
- 直接cas把索引i又null置为fwd
-
否则判断f的hash值fh是否等于MOVED
- 表示此索引下数据已经迁移完成
- advance置为true
-
否则执行具体的迁移逻辑
- 对syncronized对索引i下的首结点f加锁
- 判断索引i处结点是否等于f
-
判断fh是否大于等于0
-
此索引下为单链表结构
-
计算fh&n的值runBit
-
for循环链表,查找结点hash&n结构不一样的
- 一般情况下该链表中hash都是相等的,计算结果也相等;不相等的情况另外讨论;
-
判断runBit等于0
- ln指向lastRun;hn置空
-
否则
- hn指向lastRun;ln置空
-
处理链表结点hash不同的情况
-
setTabAt()吧nextTab索引i处置为ln
-
setTabAt()吧nextTab索引i+n处置为hn
-
setTabAt()吧tab索引i处置为fwd
-
-
否则判断f是否为TreeBin结点
-
执行红黑树的迁移
-
for循环遍历first指向的双向链表
- 以当前结点e的hash,key,value构建新的结点p
- 判断如果结点hash值h&n等于0
- p链接到双向链表lo中
- lc计数+1
- 否则p链接到双向链表hi,hc计数+1
-
判断如果lc计数小于阈值6,ln指向lo转换后的单链表;否则判断hc是否不等于0
- 不等于指向lo转换后的红黑树
- 等于0指向原索引i下结点f
-
hn做相同的逻辑
-
setTabAt()吧nextTab索引i处置为ln
-
setTabAt()吧nextTab索引i+n处置为hn
-
setTabAt()吧tab索引i处置为fwd
-
-
- 判断索引i处结点是否等于f
- 对syncronized对索引i下的首结点f加锁
-
问题:
- 源代码2433和2468行-hash数组大小调整,数据迁移的时候为什么要根据hash&n的结果来决定迁移到新hash数组中的索引位置?为什么运算结果为0迁移到索引i处而非0迁移到i+n处?
- 源代码2417行-在上面数据迁移判断i < 0 || i >= n || i + n >= nextn为true且cas确定最后一个迁移线程后,为什么没有直接收尾,结束程序而是又执行了一遍for循环呢
- 源代码2427行-在执行具体迁移逻辑加锁后,为什么还要通过tabAt()判断索引i出的结点是否等于f呢?f不是通过tabAt()获取的索引i处的结点吗?
- 2432行-什么情况下某个索引下结点的hash值会不同呢?
5 addCount()-增加计数
源码如下:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
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;
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
执行流程如下:
- 判断计数单元数组as不等于null或者cas设置baseCount为+x失败
- 判断as为空或者as的长度小于1或者当前线程计算对应的as索引值a为空或者cas设置a为+x失败
- 执行fullAddCount()方法,return
- 如果check小于等于1,return
- 执行sumCount()赋值s
- 判断as为空或者as的长度小于1或者当前线程计算对应的as索引值a为空或者cas设置a为+x失败
- 判断如果check大于等于0
- 以下为执行扩容的判断与执行逻辑,不在详述
问题:
- 2275行-此处扩容逻辑与helpTransfer()有何不同?
6 fullAddCount()
之前LongAdder()有这部分知识,这里只做简要分析:
- 如果计数单元数组不为空
- 当前线程对应的数组索引值为空,创建CounterCell
- 不为空cas+x
- 多次cas失败,执行数组扩容
- 否则判断计数单元数组为空,初始化
- 否则cas设置baseCount
问题:
- 某个线程在执行扩容counterCells之前,经历了几次cas失败?
7 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;
}
size计算发生在put,remove等改变集合的操作中,多线程访问的情况下,sumCount()计数并不完全准确。
8 remove() 删除
删除执行流程可参考put(),问题:
- 1156行-为什么在移除红黑树结点返回true的时候会直接cas设置为untreeify(t.first)后的单链表结构?
后记
如有问题,欢迎交流讨论。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent
参考:
[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p281~p289.