JAVA并发容器源码分析【二】ConcurrentHashMap分析之扩容
扩容触发条件:
- treeifyBin方法中将链表转换成红黑树时判断table数组的长度是否小于阈值(64),如果小于就进行扩容而不是树化。
- putAll方法中,会根据size大小扩容。
- putVal在计数的时候,判断当前Entry数量是否超过阈值,如果超过就进行扩容
与扩容相关的常量、变量与函数
1 |
|
协调多个线程如何调用transfer方法进行hash桶的迁移
tryPresize
协调多个线程如何调用transfer方法进行hash桶的迁移,tryPresize在putAll以及treeifyBin中被调用。
1 | /** |
addCount
这个方法一共做了两件事:更新baseCount的值,检测是否进行扩容。
1 | /** |
helpTransfer
协助扩容方法,多线程下,当前线程检测到其他线程正进行扩容操作,则协助其一起扩容。
(只有这种情况会被调用)从某种程度上说,其“优先级”很高,只要检测到扩容,就会放下其他工作,先扩容。
调用之前,nextTable一定已存在。
1 | /** |
扩容操作
扩容操作的核心在于数据的转移,多线程操作时,把整个table数组当做多个线程之间共享的任务队列,然后只需维护一个指针,
当有一个线程开始进行数据转移,就会先移动指针,表示指针划过的这片bucket区域由该线程负责。
这个指针被声明为一个volatile整型变量,它的初始位置位于table的尾部,即它等于table.length,很明显这个任务队列是逆向遍历的。
一个已经迁移完毕的bucket会被替换成ForwardingNode节点,用来标记此bucket已经被其他线程迁移完毕了。
ForwardingNode是一个特殊节点,可以通过hash域的虚拟值来识别它,它同样重写了find()函数,用来在新数组中查找目标。
数据迁移的操作位于transfer()函数,多个线程之间依靠sizeCtl与transferIndex指针来协同工作,每个线程都有自己负责的区域,
一个完成迁移的bucket会被设置为ForwardingNode,其他线程遇见这个特殊节点就跳过该bucket,处理下一个bucket。
transfer源码:
1 | /** |
从源码可知,transfer方法分为4个步骤:
- 对后续要使用的变量做初始化,如果新建临时表,容量为老表的2倍。
- 为当前线程分配任务和控制当前线程的任务进度,描述了如何与其他线程协同工作。
- 最后是具体的迁移过程(对当前指向的bucket),这部分的逻辑与HashMap类似,
拿旧数组的容量当做一个掩码,然后与节点的hash进行与操作,可以得出该节点的新增有效位,
如果新增有效位为0就放入一个链表A,如果为1就放入另一个链表B,
链表A在新数组中的位置不变(跟在旧数组的索引一致),链表B在新数组中的位置为原索引加上旧数组容量。
图形分析
1、ConcurrentHashMap支持并发扩容,实现方式是,将表拆分,让每个线程处理自己的区间,如图:
2、每个线程在处理自己桶中的数据
扩容前的状态:
当对 4 号桶或者 10 号桶进行转移的时候,会将链表拆成两份,规则是根据节点的 hash 值取于 length,如果结果是 0,放在低位,否则放在高位。
因此,10 号桶的数据,黑色节点会放在新表的 10 号位置,白色节点会放在新桶的 26 号位置。
下图是循环处理桶中数据的逻辑:
处理完之后,新桶的数据是这样的:
参考文档:
https://blog.csdn.net/u010723709/article/details/48007881
https://swenfang.github.io/2018/06/03/Java%208%20ConcurrentHashMap%20%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/
http://cmsblogs.com/?p=2283#i-4
http://www.importnew.com/29832.html
https://www.jianshu.com/p/2829fe36a8dd
https://blog.csdn.net/elricboa/article/details/70199409