1.扩容流程图
2.链表迁移示意图
3.transfer()方法
总结:
- 第一次进来扩容的线程会创建出一个新表。长度为原来的
2倍
。 - 迁移元素从后往前
(索引从大到小)
。 - 迁移完成的桶在当前桶位置放一个ForwardIngNode类型的节点,表示该桶迁移完成
- 迁移时通过
hash&n(原长度)就是判断高位
来判断在新链表中的索引位置,跟HashMap扩容原理一样。 - 低位链表(树)存储到新表的原索引位置
- 高位链表(树)存储到新表的
原索引 + n(原数组长度)
的位置 - 迁移元素时会使用synchronized锁定当前桶位,
锁对象就是当前桶位的头结点
,这是分段锁的思想。 - 迁移时,会根据原桶中的节点创建一个新的节点
(除了lastRun机制的节点外)
。 - 最后一个扩容的线程在退出时会重新扫描原表判断是否有遗漏的桶没有迁移节点,然后将nextTable赋值给table,然后将nextTable置为NULL,将sizeCtl设置为新数组长度的3/4即扩容阈值。
LastRun机制
LastRun机制就是为了省掉最后连续一段高位相同的节点的创建过程(只有原桶位是链表是才会有lastRun机制)
当最后一段的节点高位相同时,就不必创建了,直接引用这一串节点即可。
源码解析
/*
* 两种情况会进入transfer中
* 1. 触发扩容的线程 传来的nextTab属性是NULL。
* 2. 协助扩容的线程 传来的nextTab非NULL。
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
/*
* n表示扩容之前table数组的长度
* stride表示分配给线程任务的步长 (一般为16)
*/
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; //16
/*
* nextTab == null 说明当前是扩容的线程
* 需要将nextTable创建出来。
*/
if (nextTab == null) {
// initiating
try {
//创建了一个是原来长度2倍的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数组,方便协助扩容线程拿到新表。
nextTable = nextTab;
//记录迁移数据整体位置的一个标记。index计数是从1开始计算的,从高到底进行计数
//transferIndex <=0 表示迁移完毕
transferIndex = n;
}
//表示新数组的长度
int nextn = nextTab.length;
/*
* fwd节点,当某个桶位数据处理完毕后,将此桶位设置为fwd节点,其他写线程或读线程看到
* 后,会有不同的逻辑
*/
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//推进标记
boolean advance = true;
//完成标记
boolean finishing = false; // to ensure sweep before committing nextTab
/*
* i表示分配给当前线程任务,执行到的桶位
* bound 表示分配给当前线程任务的下界限制
* (迁移从高索引为低索引位开始迁移)
*/
int i = 0, bound = 0;
//自旋
for (;;) {
/*
* f 表示桶位的头结点
* fh 头结点的hash值
*/
Node<K,V> f; int fh;
/*
* 1.给当前线程分配任务区间
* 2.维护当前线程任务进入(i表示当前处理的桶位)
* 3.维护map对象全局范围内的进度
*/
while (advance) {
/*
* nextInde 分配任务的开始下标
* nextBound 分配任务的结束下标
*/
int nextIndex, nextBound;
/*
* CASE1:
* 条件一&