一、ConcurrentHashMap扩容过程

1、ConcurrentHashMap扩容时新建数组

1.1 每个线程负责的数据迁移区域的长度:stride

1.2 关于transferIndex的说明

2、ConcurrentHashMap扩容时获取迁移数据区域

2.1 总结

3、判断数据迁移是否结束

3.1 每个线程完成自己区域内的数据迁移的判断条件

3.2 如何判断整个旧数组中的数据有没有迁移完

4、ConcurrentHashMap的数据迁移

4.1、ConcurrentHashMap数据迁移的原理

4.2、ConcurrentHashMap数据迁移的源码分析

  • 4.2.1 Node链表迁移的源码分析

    • 4.2.1.1 ConcurrentHashMap中如何确定节点在哪一条链表

    • 4.2.1.2 lastRun和链表数据的迁移流程

    • 4.2.1.3 链表迁移的思考

  • 4.2.2 红黑树迁移的源码分析

5、ConcurrentHashMap的数据迁移

5.1、ConcurrentHashMap扩容引起的数据丢失问题的原因及解决办法

二、多线程扩容问题

在put过程中,有2处地方会触发扩容的情况:

  • 在put完成之后,更新元素个数时发现元素个数已经超过扩容阈值sizeCtrl,这个时候就会触发扩容(addCount方法);

  • 在链表超过8,但是数组长度 小于 64时,不会将链表转换成红黑树,而是会选择扩容数组(tryPresize)

回到正文多线程环境下扩容与单线程环境的扩容有什么不同 ?

相同点:

  • 1、首先都需要新建一个数组用于扩容后的新容器;

  • 2、将现容器的数据迁移到新的容器中;

不相同点:

在单线程中所有的操作都是只有一个线程按顺序操作,而多线程则可能同时有多个线程操作同一件事;如果按照单线程的做法对扩容过程不加限制,会产生很多问题;比如在单线程中:创建新数组的操作,在多线程中旧可能出问题;如果多个线程同时扩容就可会创建多个数组;在迁移数据的同时又有新数据添加进来又该如何处理。。。因此在多线程环境下还必须要考虑到,数据迁移过程中可能出现对原数据的添加,删除,查询等问题。

回到正文多线程环境下扩容与单线程环境的扩容有什么不同 ?

  • 多线程环境下触发扩容条件之后,如何保证只有一个线程去新建新数组 ?

  • 在数据迁移过程中如果有数据的添加,删除,查询该怎么处理 ?

  • 在ConcurrentHashMap中是多个线程同时扩容的,那么如何协调多个线程同时扩容;

  • 如何确保数据全部迁移完成?

三、ConcurrentHashMap扩容过程

在ConcurrentHashMap中多个线程同时扩容时如何协同扩容呢 ?答案是:每个线程负责一部分固定的区域

扩容的源码比较多,因此先对扩容流程有一个整体的印象;再读源码;

1、ConcurrentHashMap扩容时新建数组

1.1、每个线程负责的数据迁移区域的长度:stride

在ConcurrentHashMap中每个线程的数据迁移区域长度stride 不是一个固定值,stride 的值是根据:数组长度和cpu的个数决定的;

stride计算分2步:

  • (NCPU > 1) ? (n >>> 3) / NCPU : n ;计算出stride的值

  • 与默认值比较;小于默认值使用默认值;大于默认值使用计算出的值;

1.2、关于transferIndex的说明

我们要确定一个线程的数据迁移区域,一个长度是不行的;还必须要知道一个数据迁移的起始的位置,这样才能通过:起始位置+长度;来确定迁移的范围;而transferIndex 就是确定线程迁移的起始位置;每个线程的起始位肯定都不同,因此这个变量会随着协助扩容的线程增加而不断的变化;

在ConcurrentHashMap中,分配区域是从数组的末端开始,从后往前配分区域,

  • 第一个线程起始迁移数据的下标:transferIndex -1(数组最大下标),分配区域:[transfer-1,transfer-stride],

  • 第二线程的起始位置:(transferIndex - stride-1),分配区域:[transferIndex - stride-1,transferIndex - 2 * stride],依次类推

2、ConcurrentHashMap扩容时获取迁移数据区域

当有新线程协助扩容时首先要获取到一个起始位置才能确定负责迁移数据的范围。ConcurrentHashMap是如何处理的?因为需要考虑到多个线程同时竞争同一个起始位置,因此要使用CAS竞争让线程抢位置,竞争失败的线程则进入循环下一次继续尝试获取一个起始位置;【在ConcurrentHashMap中大量的使用了CAS来修改变量值,如果关于CAS还有疑惑,可以看这篇文章中关于CAS的部分:关于CAS】

CAS 竞争位置,竞争成功之后由变量 i,bound 记录当前线程负责迁移数据的区域

2.1、总结

每个线程迁移数据时只能迁移获取到区域的数据:按顺序遍历的将获取到的区域: [i,bound]上每一个位置的数据完全迁移;但这里要注意下:i > bound ,迁移时顺序是 : i -> bound;

通过transferIndex 判断还有没有空闲的区域;transferIndex = 0 ;表示没有空闲区域了,所有区域都有线程负责迁移数据;

如果参与参与扩容的线程较多,那么可以将不同区域分配给不同线程;但如果参与扩容线程较少,那么线程完成了分配区域的数据迁移之后,获取下一块区域继续迁移数据,直到数据迁移完为止(为了保证在参与扩容线程很少时,也能将数据完全迁移);

A、有较多线程参与扩容时:

B、参与扩容线程较少:

四、 判断数据迁移是否结束

4.1、每个线程完成自己区域内的数据迁移的判断条件

  • i < 0 ;

  • i >= n ;

  • i + n >= nextn ;

对第一个条件 i < 0; 满足这个条件的有3种情况:

  • 被分配到[0,stride]区域的线程,在完成数据迁移后,i 自减到 -1 ;

  • 没有分配到 [0,stride] 区域的线程完成了分配区域的数据迁移之后,数组中没有空闲区域需要线程来迁移数据;这个时候会将i设置为-1,进入到这个分支;

  • 没有分配到迁移区域的线程;也会将i设置为-1 ;

这三种情况都可以看作:当线程完成了分配区域内的数据迁移就会将 i 设置 为 -1;

关于判断条件 i >= n 和 i + n >= nextn 的疑惑

  • 对于后面2个判断,感到有点疑惑;n = 旧数组的长度; nextn = 新数组长度; 在ConcurrentHashMap中,数组每次扩容都是2倍;因此:nextn = 2 * n;这就可以看出 i >= n 和 i + n >= nextn;是完全等价的;相当于一个条件,没有必要同时写这2个判断;

  • 关于 i :取值范围是旧数组的下标区间[0,n-1];i 的最大取值也就是(n-1) ;在这个分支内部设置了 i = n ; 即使是在这里将i 的值设置为n,但是进入下一次循环的时候,i自减1,到这个分支也不会出现 i = n 的情况,更没有 i > n 的情况出现;因此对什么时候出现 i >= n 的情况暂时还没想到;

4.2、如何判断整个旧数组中的数据有没有迁移完

在ConcurrentHashMap中为了确定旧数组的数据被完全的迁移了,让最后一个完成数据迁移的线程在完成迁移之后,再重新遍历检查整个数组;遍历从后往前遍历 i : (n-1) -> 0 ; 在遍历过程中,发现有位置上的数据没有迁移,就迁移数据;这样当遍历结束之后,就可以确定整个数组的数据已经被迁移完了;

那具体是怎么做的呢?我们继续往下分析

进入这个分支后,内部第一个分支的判断条件只有一个标志量:finishing=true; 表示整个数组的数据已经全部迁移完;这部分比较简单,数据迁移完之后更新了以下几个变量的值:

  • nextTable=null,因为已经完成数据;

  • 将扩容后的数组nextTab赋值给table;

  • 设置扩容阈值 sizeCtrl = 0.75 * n;要看懂第二个分支里对sizeCtrl的处理就要了解在线程参与扩容前对sizeCtrl做了哪些处理?在线程扩容时对sizeCtrl的值做的处理:

  • 创建扩容数组的线程,将sizeCtrl 的值设置为:sizeCtrl = ( resizeStamp(n) << RESIZE_STAMP_SHIFT ) + 2 ;

  • 其余参与扩容的线程:每当有线程参与协助扩容 :sizeCtrl += 1;

在上节分析过:进入该分支的线程,都已经完成了该线程所分配区域的数据迁移,并且此时旧数组中,没有还未分配的区域;也就是说所有区域都分配完了,自己区域内的数据也迁移完成;那么在完成了数据迁移之后,设置: sizeCtrl -= 1;表示有一个线程完成了数据迁移;对于判断条件:(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT;

我们将公式稍作变形:sc != (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 不等式的右边这正是创建扩容数组时给sizeCtrl的初始值;如果相等,那就说明这是最后一个线程;

在这第二个分支内做的处理:

  • 如果不是最后一个退出扩容的线程,就直接退出扩容;

  • 如果是最后一个退出扩容的线程:i = n ,finishing = true 扫描全表,检查是否有没被迁移的数据,如果有就将其迁移到新数组检查完整个数组之后,将table更新为新数组:nextTab完成扩容然后退出;

五、ConcurrentHashMap的数据迁移

5.1、ConcurrentHashMap数据迁移的原理

在讲这之前要搞清楚迁移数据时要解决的问题:同一条链表(红黑树)的节点在数组扩容之后可能不在同一条链表(红黑树)上;因此不能直接将链表头节点(红黑树root节点)迁移到新数组来完成这条链表(红黑树)的迁移。

在ConcurrentHashMap中,数组table的长度为2n大小,这个设计真的很方便扩容;扩容后节点的下标只有两种情况:下标的值不变 或者 下标值:i + n; (n扩容前数组长度)

例子:

我们看到同一个 node 扩容后:下标只有最高位有变化,比之前多了一位不确定 0 或 1 的高位,其余位置不变;最高位 x 只有2个值,因此 i 的下标也只能有2种取值:

  • 高位 x = 0;下标不变

  • 高位 x = 1;下标:i + n(i: 扩容前下标,n:扩容前数组长度)

在ConcurrentHashMap扩容时,线程在数据在迁移时:一条链表最多可能分化为 2条Node链表;因此在迁移链表时就只需要构造2条链表即可转移该链表所有的节点;

对于红黑树来说也是一样的:在同一棵红黑树上的节点对应了同一个数组位置,在扩容的时候,红黑树节点也只会有2个位置可以选择;因此红黑树也会分成 2条TreeNode链表;一个在原位置,一个在:i + n位置;你或许会有疑问,

明明是红黑树,怎么会分成2条链表?在由Node链表 -> 红黑树时;首先是将Node链表 -> TreeNode链表;然后将TreeNode链表 -> 红黑树;在转换成红黑树之后,保留了TreeNode链表的头节点;也就是说,TreeNode同时保存2种结构:

  • TreeNode的链表结构,可以通过表头以及TreeNode中的next属性遍历整个链表;

  • TreeNode的红黑树结构,可以通过root根节点以及左右子节点遍历整颗红黑树;

将TreeNode当成链表来看,就只关注TreeNode中的next属性,以表头first节点开始,通过next属性就可以遍历完整个TreeNode链表;

将TreeNode当成红黑树来看就不能关注next节点,要关注它的left,right子节点以及parent父节点这些属于树的属性;搜索方法也是依靠二叉搜索树从root节点通过左右子节点来遍历整颗红黑树;

最后还有一个问题:当红黑树put值时,怎么解决既在链插入又在红黑树中插入 ?

  • 在链表上插入体现为:能成为链表中一个节点的next节点,或者链表中一个节点的前驱节点;

  • 在红黑树的插入体现为:是成为某个红黑树节点的left/right节点;==》关键就是找到父节点;

在ConcurrentHashMap中,TreeNode链表的头节点是记录在TreeBin中的,因此它使用了非常简单高效的做法,直接将新插入的节点当作表头,将原头节点当作新表头的next节点;对于红黑树找父节点,就按照二叉搜索树的插入方法去找到parent节点即可;

最后,我们在迁移红黑树时,使用TreeNode的链表结构来遍历TreeNode链表,同时构建2个TreeNode链表来迁移数据;在将TreeNode链表遍历完之后,分别判断2条链表的长度,来决定应该重新构建红黑树还是将TreeNode转换成Node链表;

最后再分别将红黑树分裂形成的2个红黑树TreeBin或者是链表表头Node添加到新数组中;

TreeNode的结构:

5.2、ConcurrentHashMap数据迁移的源码分析

5.2.1、Node链表迁移的源码分析

					//f = table[i];
//fh = f.hash;
//n = table.length;(旧数组长度)
//ln:所在的链表的位置,没变还是在下标:i
//hn:链表所在位置:i+n;
Node<K,V> ln, hn;
if (fh >= 0) {//Node的hash值不为负数
//只有0 和n两种取值;结果是0,表示扩容后位置不变;结果不为0,表示扩容后位置:i+n;
int runBit = fh & n;
Node<K,V> lastRun = f;
//循环遍历旧数组中,table[i]所在链表遍历链表找出lastRun
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
} //根据runBit的值,给ln,hn赋值;
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//遍历链表将节点分配到ln,hn中;
//新Node插入到表
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);
}
//遍历完旧数组的链表后,重新生成了ln,hn2条链表;
//ln链表插入到新数组的下标:i
setTabAt(nextTab, i, ln);
// hn插入到:i+n位置;
setTabAt(nextTab, i + n, hn);
//并将旧数组的i位置插入扩容标志节点:ForwardingNode对象;
setTabAt(tab, i, fwd);
advance = true;
}

这里再简单提下:在ConcurrentHashMap中table数组可能存在四种Node节点

  • Node:普通node节点,表示链表;节点的hash值大于0;

  • ForWardingNode节点,扩容标志;hash = MOVE(-1);在这个对象中有nextTab对象,协助扩容时根据nextTab找到扩容后新数组;

  • TreeBin节点,表示红黑树;hash = TREEBIN(-2)在这个对象中存储了TreeNode链表表头first,以及红黑树根节点root;

  • 调用computeIfAbsent方法put值当hash定位到下标 i 时,table[i]=null,会使用:ReservationNode对象来占位;

5.2.2、ConcurrentHashMap中如何确定节点在哪一条链表

在上一节中,分析到可以使用新数组的容量来计算出node节点迁移后的坐标;在ConcurrentHashMap迁移链表时分裂的2个新链表分别是:ln(下标:i),hn(下标:i+n);在判断node节点应该在哪条链表不是直接使用下标判断,而是利用扩容后下标位置有没有发生改变来判断;没有改变就在ln链表,位置改变了就在hn链表;这个时候直接使用扩容后的容量计算出下标就不能直接区分坐标到底是变了还是没有变;因此ConcurrentHashMap使用了另一种方法来判断扩容后下标变不变:fh & n 即 hash & oldLength(旧数组长度);这个方法可以直接计算出扩容后,node 坐标的位置的偏移量;

  • fh & n = 0;扩容后下标不变;

  • fh & n = n;扩容后下标位置改变(i + n);

5.2.3、lastRun和链表数据的迁移流程

lastRun是理解链表迁移的关键;要从整体来看lastRun,理解他在链表转移数据的过程有什么用;但是不好组织语言来描述;

我们知道一条链表上的节点,在扩容之后会分成2条链表;而这条链表上有哪些节点扩容之后位置要改变的哪些节点位置是不改变的,这两种节点在链表上的分布是不确定的;我们可以将连续相同的节点看作是一段链表(一个节点也是一段链表),那么这条链表就可以看作2种链表组合组成的;而lastRun,是指向最后一段链表的头节点;再配合runBit(偏移量)就可以确定最后一段链表属于哪种节点;因此可以看到再次遍历数组时,遍历到lastRun就结束遍历;

还是用一个例子,画出整个流程图就很好理解;

  • 1、遍历链表找出lastRun:

  • 2、根据runBit的值来确定lastRun属于哪一条链表;

    • runBit = 0;位置不变属于ln链表 :ln = lastRun;

    • runBit != 0; 位置改变属于hn链表 : hn = lastRun;

  • 3、重新遍历链表,将链表中的节点根据hash值计算出的偏移量选择插入的链表:ln,hn中;新插入的节点插在表头;当next指针指向与lastRun相同的对象时遍历结束,此时链表所有的节点都已经迁移到两个新链表中了;

  • 4、将ln,hn的值插入到新数组nextTab;将table[i] = ForwardingNode对象;

5.2.4、红黑树迁移的源码分析

红黑树的迁移逻辑要比链表的迁移逻辑要简单得多(PS:这才是正常人的逻辑);没那么多弯弯绕绕的逻辑,就是一把梭~在上面 数据迁移的原理部分,讲到过红黑树的TreeNode节点其实也维护了一个链表的结构;因此迁移时直接使用TreeNode的链表结构,迁移逻辑就会很简单了;

与Node链表的迁移不同;红黑树的迁移是直接循环遍历TreeNode链表,利用hash值计算偏移量来决定TreeNode应该放到哪个链表上;同时插入的位置是在表尾;因此可以看到源码中,一个链表由表头表尾共同维护;在遍历完整个TreeNode的节点之后,再判断TreeNode链表是否应该转换成红黑树,还是退化成Node链表;

源码:

                    else if (f instanceof TreeBin) {//判断是不是红黑树节点
TreeBin<K,V> t = (TreeBin<K,V>)f;
//lo:TreeNode链表表头:下标位置不变:i
//loTail:表尾,每次插入节点都在表尾插入 TreeNode<K,V> lo = null, loTail = null;
//hi:TreeNode链表表头:下标位置:i+n;
//hiTail :表尾,每次插入节点都在表尾插入
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) {//偏移量 = 0插入loTail
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;//lo链表插入表尾
loTail = p;//更新lo链表表尾
++lc;//记录lo链表节点个数
}
else {//偏移量 != 0插入hiTail
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;//hi链表插入表尾
hiTail = p;//更新hi链表表尾
++hc;//记录hi链表节点个数;
}
}
//分别判断ln,hn节点个数是否满足生成红黑树的条件;
//不满足则将TreeNode链表转换成Node链表;
//满足则将TreeNode节点重新生成红黑树;
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;
//将ln,hn分别设置到新数组的i,i+n位置
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//在table[i]中设置ForwardingNode对象;
setTabAt(tab, i, fwd);
advance = true;
}

流程:

  • 1、遍历TreeNode链表,利用hash值计算扩容后偏移量(hash & n);根据偏移量是否为0来选择将节点添加到哪个链表。新加入的节点放到链表表尾,同时统计链表元素个数;

  • 2、根据元素个数判断是否将链表转换成红黑树;

  • 3、将:lo,hi分别迁移到新数组nextTab的:i,i+n位置;在旧数组table被迁移数据的位置:i,设置一个ForwardingNode对象;

六、扩容逻辑的整体分析

只保留整体大框架,省略掉细节部分:

    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
}
}
第三部分:判断table元素的节点类型;
//如果table[i]=null,就将fwd对象填充到这个位置,表示正在扩容;
//其他线程看到了会通过fwd中的信息找到nextTab协助扩容;
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//判断位置上的节点是否是ForwardingNode对象,如果是表示已经有线程迁移了该位置的数据
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
第四部分:迁移数据;
else {
//获取锁
synchronized (f) {
//获取到对象f的锁之后,会对比:获得锁前后f对象有没有花生改变,如果发生改变,就不处理
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
处理链表
}
else if (f instanceof TreeBin) {
处理红黑树 }
}
}
}
}

整体过程可以分为以下几部分:

  • 获取数据迁移区域;

  • 结束扩容的判断;

  • 判断table[i]的类型:如果是null;就设置ForwardingNode对象

  • 判断table[i]的类型:如果是ForwardingNode对象则,迁移下个位置的数据

  • 迁移Node链表或红黑树的数据

ConcurrentHashMap扩容引起的数据丢失问题的原因及解决办法

ConcurrentHashMap扩容导致丢失数据的原因

在最后的部分,关于数据迁移:首先要获取f(table[i])的对象锁;获取到对象锁之后,会比较获取锁前后,table[i]的值f是否发生改变;为什么会做这个判断呢?因为在扩容期间,

ConcurrentHashMap还是允许其他线程的:put,get,remove操作;其中put,remove操作,可能会更改table[i]的值;

笔者认为原因有以下几点:

  • put操作导致:Node链表 -> 红黑树;

  • remove操作直接将table[i],remove掉了;

  • remove操作导致:红黑树 -> Node链表;

(PS:顺带一提:在remove操作完成之后会更新元素个数调用addCount方法,也就是说一个线程在remove时,也可能会遇到容器扩容的情况从而协助扩容;)

回到正文继续分析:在f发生改变的情况下,ConcurrentHashMap不会迁移该位置的数据,而是会进入循环,i 自减 1 ;进入到下一个位置的数据迁移;在这种情况下,即使线程遍历完自己的区域[i,bound],但是并不能保证能将自己区域内的数据完全的迁移完;在上诉情况下有机率造成线程在迁移数据过程中发生数据丢失的情况;

我最开始是这样认为的,经评论区Super_Xue指正(本文后续已修改);在f发生改变之后,没有进入数据迁移的分支进行数据迁移时,advance=false;回到while(advance){}循环时,不会进入到循环体中自减,因此还是会在当前位置进行数据迁移;也就是说,正常情况下每个线程分配到的区域会保证数据都迁移完;

最后一个线程一定扫描全数组吗 ?

我们可以看看它时如何让最后一个线程检查全数组的。处理非常简单:将 i 设置为 n;我们前面分析过,一个线程负责[i,bound]的区域,当最后一个线程负责的区域是[stride,0],那么将i设置成n之后,这种情况线程是可以扫描全表的;

如果最后一个退出的线程负责的区域:[i,bound]中bound不等于0,那么该线程不会不会扫描全表;

最后,画个流程图总结下:

https://blog.csdn.net/m0_37550986/article/details/125230667?spm=1001.2014.3001.5501

七、图解扩容

触发扩容的操作:

假设目前数组长度为8,数组的元素的个数为5。再放入一个元素就会触发扩容操作。

总结一下扩容条件:

(1) 元素个数达到扩容阈值。

(2) 调用 putAll 方法,但目前容量不足以存放所有元素时。

(3) 某条链表长度达到8,但数组长度却小于64时。

CPU核数与迁移任务hash桶数量分配(步长)的关系

单线程下线程的任务分配与迁移操作

多线程如何分配任务 ?

普通链表如何进行数据迁移 ?

首先锁住数组上的Node节点,然后和HashMap1.8中一样,将链表拆分为高位链表和低位链表两个部分,然后复制到新的数组中

什么是 lastRun 节点 ?

红黑树如何迁移 ?

hash桶迁移中以及迁移后如何处理存取请求 ?

多线程迁移任务完成后的操作

ConcurrentHashMap扩容过程的更多相关文章

  1. HashMap和ConcurrentHashMap扩容过程

    HashMap 存储结构 HashMap是数组+链表+红黑树(1.8)实现的. (1)Node[] table,即哈希桶数组.Node是内部类,实现了Map.Entry接口,本质是键值对. 下图链表中 ...

  2. 双系统Ubuntu分区扩容过程记录

    本人电脑上安装了Win10 + Ubuntu 12.04双系统.前段时间因为在Ubuntu上做项目要安装一个比较大的软件,导致Ubuntu根分区的空间不够了.于是,从硬盘又分出来一部分空间,分给Ubu ...

  3. jdk7和8中关于HashMap和concurrentHashMap的扩容过程总结,以及HashMap死循环

    题外话:为什么要hashcode进行spread? 充分使用key.hashCode()的高16位信息,保证hash分布更分散, 扩容操作是新建2倍于原表大小的新表,并将原表结点拷贝一份放在新表中,对 ...

  4. concurrentHashMap扩容相关方法详解

    上一个博客中说到了concurrentHashMap的put操作,在put操作之后如果添加了节点,我们首先会把全局的节点数+1,如果满足了扩容条件,我们则进行扩容 我们先从addCount方法说起 / ...

  5. ConcurrentHashMap 扩容分析拾遗

    前言 这是一篇对 transfer 方法的拾遗,关于之前那篇文章的一些一笔带过,或者当时不知道的地方进行回顾. 疑点 1. 为什么将链表拆成两份的时候,0 在低位,1 在高位? 回顾一下 transf ...

  6. ConcurrentHashMap扩容

    然后,说说精华的部分. Cmap 支持并发扩容,实现方式是,将表拆分,让每个线程处理自己的区间.如下图:     假设总长度是 64 ,每个线程可以分到 16 个桶,各自处理,不会互相影响. 而每个线 ...

  7. 阿里面试题:说说HashMap的扩容过程?

    这是一道阿里的面试题,考察你对HashMap源码的了解情况,废话不多说,咱们就直接上源码吧! jdk 1.7 源码 void resize(int newCapacity) { Entry[] old ...

  8. 并发编程——ConcurrentHashMap#transfer() 扩容逐行分析

    前言 ConcurrentHashMap 是并发中的重中之重,也是最常用的数据结果,之前的文章中,我们介绍了 putVal 方法.并发编程之 ConcurrentHashMap(JDK 1.8) pu ...

  9. ConcurrentHashMap原理分析(二)-扩容

    概述 在上一篇文章中介绍了ConcurrentHashMap的存储结构,以及put和get方法,那本篇文章就介绍一下其扩容原理.其实说到扩容,无非就是新建一个数组,然后把旧的数组中的数据拷贝到新的数组 ...

  10. HashMap扩容和ConcurrentHashMap

    HashMap 存储结构 HashMap是数组+链表+红黑树(1.8)实现的. (1)Node[] table,即哈希桶数组.Node是内部类,实现了Map.Entry接口,本质是键值对. stati ...

随机推荐

  1. Pycharm:鼠标滚动控制字体大小

    Pycharm字体放大的设置 1.File -> setting -> Keymap ->在搜寻框中输入:increase -> Increase Font Size(双击) ...

  2. ABC243

    ABC224 D 题目大意 有一个九个点的无向图棋盘,上面有八个棋子,一次操作能将一个棋子沿边移到空点上,问将每个棋子移到与它编号相同的点最少几步. 解题思路 考虑使用 BFS. 用 string 存 ...

  3. ef 值转换与值比较器

    前言 简单介绍一下,值转换器和值比较器. 正文 为什么有值转换器这东西呢? 那就是这个东西一直必须存在. 比如说,我们的c# enum 对应数据库的什么呢? 是int还是string呢? 一般情况下, ...

  4. 深入理解Java泛型、协变逆变、泛型通配符、自限定

    禁止转载 重写了之前博客写的泛型相关内容,全部整合到这一篇文章里了,把坑都填了,后续不再纠结这些问题了.本文深度总结了函数式思想.泛型对在Java中的应用,解答了许多比较难的问题. 纯函数 协变 逆变 ...

  5. HTML 基本骨架

    HTML 基本骨架 HTML5的骨架是构建HTML5页面的基础结构,它主要由以下几个部分组成: <!DOCTYPE html> <html> <head> < ...

  6. 1. Docker 的简介概述

    1. Docker 的简介概述 @ 目录 1. Docker 的简介概述 2. Docker 的理念: 3. 容器与虚拟机比较 4. Docker应用场景 5. 最后: 为什么会有 Docker 出现 ...

  7. 本地搭建DeepSeek和知识库 Dify做智能体Agent(推荐)

    一.基础信息 1.硬件环境: CPU >= 2 Core 显存/RAM ≥ 16 GiB(推荐) 2.软件 (1)Ollama Ollama 是一款跨平台的大模型管理客户端(MacOS.Wind ...

  8. Flink On Yarn的两种部署模式

    一.内存Job管理模式yarn-per-job 使用介绍:常用的模式 二.内存集中管理模式yarn-session 使用介绍:当作业很少并且都较小,能快速执行完成时,可以使用.否则一般不会使用该模式 ...

  9. Mac安装Scala2.12

    一.下载Scala brew install scala@2.12 二.设置环境变量 vim ~/.bash_profile export SCALA_HOME=/usr/local/opt/scal ...

  10. 百万架构师第三十九课:RabbitMq:Linux安装RabbitMq|JavaGuide

    来源:https://javaguide.net RPM包安装RabbitMQ RabbitMQ的安装非常简单,由于RabbitMQ依赖于Erlang,所以需要先安装Erlang,解决依赖关系后,就可 ...