扩容解析
addCount操作容器大小后扩容
private final void addCount(long x, int check) {
...省略count加减逻辑,详细见上一章节
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//sizeCtl 0.75 * 数组长度 且table不为null 且 table长度小于最大值
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//n为数组长度,以16为例
//Integer.numberOfLeadingZeros(16) | (1 << (16 - 1))
//32795
int rs = resizeStamp(n);
//sc = sizeCtl
//sizeCtl在初始化initTable里赋值为0.75n,16为例等于12
//肯定大于0
//其他线程cas竞争sizeCtl失败,此时sc为负数
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//其他线程帮助扩容,有一个帮助线程sc + 1
//最终判断扩容结束条件为sc = 最初设置值
//表示初始扩容线程结束,帮助线程扩容结束
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//(rs << RESIZE_STAMP_SHIFT) + 2) n为16为例等于-2147483619
//cas设置sizeCtl为 负很大数
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
//统计容器大小
s = sumCount();
}
}
}
主要判断 使用大小 > 0.75 * 数组长度则开始扩容,cas设置sizeCtl为负很大数
竞争sizeCtl失败线程帮助初始扩容线程一起扩容,直到初始扩容线程和帮助线程都扩容结束。
transfer扩容操作
最终扩容操作都在此方法中实现,代码很长,很复杂,慢慢读,ConcurrentHashMap最难的代码。
根据步长计算每个线程同步任务的起始下标和结束下标:
线程1扩容操作:
如上图,先说明下扩容操作,方便下面代码理解。
假设步长为2,Node数组大小为6,正好3个线程扩容。
1、先新建数组,长度为原先2倍
2、先计算线程扩容的i起始下标和结束下标bound,每个线程对应的开始结束下标不同
3、如图,线程1bound为5,i4,表示线程1先同步Node5,将Node5同步到新的数值中,同步完将老数组的5位置赋值为ForwardingNode节点,表示5节点同步结束。然后i起始下标移动到4位置,同理开始同步4节点。
4、4节点同步结束,会继续请求新的起始下标和结束下标,存在则继续扩容下一个区间,直到所有线程扩容完所有节点
5、所有线程完成后将新的数组复制给table,覆盖老数组
上源码:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//计算步长 最小16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
//新table为空则初始化,大小为原先2倍
if (nextTab == null) {
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
//transferIndex为扩容前数组大小
transferIndex = n;
}
int nextn = nextTab.length;
//ForwardingNode继承于Node,新建hash为-1的Node
//Node节点为ForwardingNode表示已扩容完成,废弃
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
扩容完当前节点,是否需要继续扩容其他节点
boolean advance = true;
//当前线程扩容逻辑是否结束
boolean finishing = false;
//自旋
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//计算线程扩容的i起始下标和结束下标bound
while (advance) {
int nextIndex, nextBound;
//初始时i bound都为0,不会进来
//i-1 大于等于 bound,表示任在同步区间,不需要计算
if (--i >= bound || finishing)
advance = false;
//表示扩容完成,transferIndex为0,i为-1
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//cas设置 transferIndex为 扩容前数组大小-步长
//所有每个线程过来计算的范围不一样
//或者只有一个线程,当扩容完后面位置,重新计算起始结束位置,继续循环
//下一个区间
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;
//扩容完成,nextTab赋值给table
//sizeCtl
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//sizeCtl 初始化扩容时有个初始值,如10
//其他线程put时发现数组在扩容,会帮助扩容,sizeCtl+1
//扩容完则sizeCtl-1
//如果此时sizeCtl等于初始值,表示 帮助线程扩容都完成 且 初始扩容线程扩容完成
//则表示扩容完成,finishing设为true
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
}
}
//取数组[i]的元素,为空直接将其设为fwd,表示此位置废弃
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//当前节点是fwd已废弃
//advance为true,重新进入while循环
//-i >= bound
//i减1,下标向前移动,直到移动到bound位置
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//对f加锁,防止其他线程操作
synchronized (f) {
//判断位置是否变化,没有变化迁移链表或红黑树
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//hash 大于等于0 表示链表
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//循环链表,f为链表首元素,直到f.next没有值为止
//计算链表尾元素
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);
}
//cas设置新数组i和i + n位置元素
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//cas老数组位置设置为fwd,已废弃
setTabAt(tab, i, fwd);
advance = true;
}
//f节点为TreeBin,表示f是个红黑树
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 hn 是否需要转换为链表,否则new TreeBin
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;
//设置新tab的i i+n 位置为ln hn
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//将旧tab的f位置设为fwd,废弃
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
针对线程算起始结束位置的算法,提出来具体看下
int i = 0, bound = 0;
boolean advance = true;
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;
}
}
以上扩容图为例,数组长度6,stride步长为2
nextIndex <-- transferIndex 为扩容前数组大小
线程1开始计算:
transferIndex = 6 nextIndex = 6
cas设置transferIndex = 4(nextIndex > stride)
nextBound = 4 bound = 4
i = 5
则线程1计算出的范围为 i5 -bound 4
线程2开始计算:
transferIndex = 4 nextIndex = 4
cas设置transferIndex = 2(nextIndex > stride)
nextBound = 2 bound = 2
i = 3
则线程2计算出的范围为 i3 -bound 2
线程3开始计算:
transferIndex = 2 nextIndex = 2
cas设置transferIndex = 0(nextIndex > stride)
nextBound = 0 bound = 0
i = 1
则线程3计算出的范围为 i1 -bound 0
得出此算法,算出线程扩容的处理区间,处理完成后重新算出下一个区间,多个线程扩容则竞争得出每个扩容区间,然后执行区间内的同步任务。
区间内的同步任务主要步骤:
1、扩容节点为null,直接用fwd节点覆盖老的数组对应下标
2、扩容节点为fwd,表示已扩容完成
3、扩容节点正常,则开始向新数组同步节点,先对节点上锁,防止同步区间其他线程使用,扩容完成用fwd节点覆盖老的数组对应下标
4、向做移动起始坐标i,处理区间前一个节点(右 -> 左)
5、当处理到起始坐标 = 结束坐标,表示区间任务完成
6、重新计算扩容区间
看下扩容结束代码
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;
}
}
transferIndex为0时,修改 i 为 -1,表示当前线程的扩容任务完成,无法再申请扩容区间,则sizeCtl - 1后退出扩容操作。
当某个线程任务完成时,sizeCtl等于初始值,表示所有线程扩容全部都完成,因为初始扩容时sizeCtl设置为某个初始值,有一个线程帮助扩容则+1,而一个线程扩容结束则-1,所有sizeCtl等于初始值,表示所有线程扩容全部都完成。finishing设为true,再次循环进入上面if逻辑,nextTab赋值给table。
新数组的下标单独说明下
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);
链表和红黑树扩容,都存在类似逻辑,通过 ph & n 操作将原先数据分为两份。
n为原先数组的长度,一般都为2 n次方,以默认16为例
.,00001011
&00001000
随便一个数,最终结果只于n为1的位数是0和1有关,结果要么0,要么1,完美的将链表或红黑树分为两份。
放入新数组的位置,一份放在原先位置,一份放在原先位置+原数组长度位置
helpTransfer帮助扩容
put操作发现Node节点为Moved,表示当前Node已扩容完成,但整个数组未扩容完成,则去帮助一起扩容,最终调用transfer方法,此方法上面已详述。总结就是去申请扩容区间,同步区间内数据至新数组。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//判断tab不为null且当前节点为ForwardingNode,已扩容完
//ForwardingNode.nextTab不为空,则帮助下个节点扩容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
//循环nextTab未改变且tab未改变,sizeCtl<0
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//设置 sizeCtl+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//执行扩容操作
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
最后
扩容操作终于写完了,真的很绕,希望我画图、分解,能帮助你理解。