一、前瞻
并发编程源码解析(四)ConcurrentHashMap源码解析之一基础概念介绍以及散列算法讲解-CSDN博客
并发编程源码解析(五)ConcurrentHashMap源码解析之二读与写源码分析-CSDN博客
二、介绍
首先,我们先来复习一下 sizeCtl 的状态,
-1 正常初始化
< -1 是正在扩容
0 没有初始化
> 0 数组扩容阈值/ 没有初始化是代表初始化长度
接着我们来介绍一下关于并发扩容的基本概念, ConcurrentHashMap 的第一个线程进入线程之后会将 sizeCtl 设置为 -2,然后将新的数组创建好,并将扩容任务拆分成若干个子任务,当其他线程进行 put 时,如果检测到了正在扩容就会将 sizeCtl +1 (会变成-3) 以表示新的线程参与并发扩容;当任务结束时线程退出扩容就会将 sizeCtl -1, 最后一个线程退出时,则会检查一遍数组是否全部迁移完毕,则会将 sizeCtl 设置为下次扩容的阈值并退出扩容。
ConcurrentHashMap 的扩容原理是非常重要的一部分内容,因为它扩容并不是由单线程执行的而是多线程并发执行的。
这过程一共会在以下几个场景体现出来,本文主要的内容是有tryPresize 作为引子来讲解真正的迁移方法 transfer。
- 转换成红黑树的 treeifyBin 方法由 tryPresize 方法触发。
- 另外一个就是 addCount 方法。
三、源码解析
3.1 tryPresize 的源码解析
3.1.1 tryPresize 的触发
tryPresize 会在当长度小于 64 时若转换成红黑树的情况,就会触发扩容方法,以减少红黑树的形成。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//当长度小于64时触发扩容方法
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 省略部分代码
}
}
}
3.1.2 tryPresize 源码解析
private final void tryPresize(int size) {
//如果 size 大于最大容量的一半,则赋值为最大容量
//tableSizeFor 的主要功能是让不等于二的n次幂的变到离它最近的比它大的二的n次幂
//如:传入9 则 变成 16
//MAXIMUM_CAPACITY 的 大小为 1 <<< 30 是因为 sizeCtl 的前两位是用来存储它的状态的
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
// 如果sizeCtl大于等于0则没有正在扩容,则触发扩容流程
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//tab为空 进入初始化流程;初始化流程已经在上期介绍过了这里不赘述
if (tab == null || (n = tab.length) == 0) {
//省略部分代码
}
// c 如果小于现在的 sizeCtl 证明已经有其他线程扩容完毕退出循环
// 现有数组长度已经达到最大值也退出循环
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 最后再检查一遍table 有没有被其他人扩容过
else if (tab == table) {
// Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1))
// 计算n前面有多少个的0,并保证他的第十六位是1;这里的目的十位确保他左移十六位后是负数
int rs = resizeStamp(n);
//这是一个bug,这里的sc不可能小于0,因为这里没有任何操作对sc赋值
if (sc < 0) {
Node<K,V>[] nt;
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);
}
//进行扩容,第一个进入的线程+2,进入扩容流程
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
3.2 transfer 方法
3.2.1 transfer方法-计算每个线程迁移的长度
// 开始扩容 tab=oldTable
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// n = 数组长度
// stride = 每个线程一次性迁移多少数据到新数组
int n = tab.length, stride;
// 基于CPU的内核数量来计算,每个线程一次性迁移多少长度的数据最合理
// NCPU = 4
// 举个栗子:数组长度为1024 - 512 - 256 - 128 / 4 = 32
// MIN_TRANSFER_STRIDE = 16,为每个线程迁移数据的最小长度
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 根据CPU计算每个线程一次迁移多长的数据到新数组,如果结果大于16,使用计算结果。 如果结果小于16,就使用最小长度16
}
3.2.2 transfer方法-构建新数组并查看标识属性
// 以32长度数组扩容到64位例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 省略部分代码
// n = 老数组长度 32;stride = 步长 16
// 第一个进来扩容的线程需要把新数组构建出来
if (nextTab == null) {
try {
// 将原数组长度左移一位(x2),构建新数组长度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
// 到这说明已经达到数组长度的最大取值范围
sizeCtl = Integer.MAX_VALUE;
return;
}
// 将成员变量的新数组赋值
nextTable = nextTab;
// 迁移数据时,用到的标识,默认值为老数组长度
transferIndex = n; // 32
}
// 新数组长度
int nextn = nextTab.length; // 64
// 在老数组迁移完数据后,做的标识, get识别到就会直接来新数组查询
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 迁移数据时,需要用到的标识
boolean advance = true;
boolean finishing = false;
// 省略部分代码
}
3.2.3 transfer方法-线程领取迁移任务
// 以32长度扩容到64位为例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// stride:16 n:32
int n = tab.length, stride;
if (nextTab == null) {
// 省略部分代码…………
// nextTable:新数组
nextTable = nextTab;
// transferIndex:0
transferIndex = n;
}
// nextn:64
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance:true,代表当前线程需要接收任务,然后再执行迁移,如果为false,代表已经接收完任务
boolean advance = true;
// finishing:false,是否迁移结束!
boolean finishing = false;
// 循环……
// i = 15 代表当前线程迁移数据的索引值!!
// bound = 0
for (int i = 0, bound = 0;;) {
// f = null
// fh = 0
Node<K,V> f; int fh;
// 当前线程要接收任务
while (advance) {
int nextIndex, nextBound;
// 对i进行--,并且判断当前任务是否处理完毕!
if (--i >= bound || finishing)
advance = false;
// 判断transferIndex是否小于等于0,代表没有任务可领取就结束任务。
// 在线程领取任务会,会对transferIndex进行修改,修改为transferIndex - stride
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,代表当前线程领取到任务了。
advance = false;
}
}
// 开始迁移数据,并且在迁移完毕后,会将advance设置为true
}
}
3.2.4 transfer方法-迁移结束操作
// 以32长度扩容到64位为例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
for (int i = 0, bound = 0;;) {
while (advance) {
// 判断扩容是否已经结束!
// i < 0:当前线程没有接收到任务!
// i >= n: 迁移的索引位置,不可能大于数组的长度,不会成立
// i + n >= nextn:因为i最大值就是数组索引的最大值,不会成立
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// finishing为true,代表扩容结束
//重新计算扩容阈值; 如:16 - 4 = 12
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 当前线程没有接收到任务,让当前线程结束扩容操作。
// 采用CAS的方式,将sizeCtl - 1,代表当前并发扩容的线程数 - 1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// sizeCtl的高16位是基于数组长度计算的扩容戳,低16位是当前正在扩容的线程个数
// 代表当前线程并不是最后一个退出扩容的线程,直接结束当前线程扩容
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果是最后一个退出扩容的线程,将finishing和advance设置为true
finishing = advance = true;
// 将i设置为老数组长度,让最后一个线程再从尾到头再次检查一下,是否数据全部迁移完毕。
i = n;
}
}
// 开始迁移数据,并且在迁移完毕后,会将advance设置为true
}
}
3.2.5 transfer方法-迁移数据(链表)
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 省略部分代码
for (int i = 0, bound = 0;;) {
// 省略部分代码
// 如果当前的桶没有数据,则无需迁移,直接将当前桶位置设置为fwd
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) {
// DCL 判断之前取出的数据是否为当前的数据。
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// hash大于0,不是红黑树,使用链表方式迁移数据
if (fh >= 0) {
// lastRun机制 这种运算结果只有两种,要么是0,要么是n;
// 目的是为了减少 hash 迁移的复杂度
// 如: 8 -> 16, 1 位置要么保持在1 要么迁移到5
int runBit = fh & n;
Node<K,V> lastRun = f;
// 循环的目的就是为了得到链表下经过hash & n结算,分别储存到高低位
// 在迁移数据时,值需要迁移到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;
}
}
// 这里就是进行了高低位赋值
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
// 循环到lastRun指向的数据即可,后续不需要再遍历
for (Node<K,V> p = f; p != lastRun; p = p.next) {
// 获取当前Node的hash值,key值,value值。
int ph = p.hash; K pk = p.key; V pv = p.val;
// 如果hash&n为0,挂到lowNode上
// 如果hash&n为n,挂到highNode上
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 采用CAS的方式,将ln挂到新数组的原位置
setTabAt(nextTab, i, ln);
// 采用CAS的方式,将hn挂到新数组的原位置 + 老数组长度
setTabAt(nextTab, i + n, hn);
// 采用CAS的方式,将当前桶位置设置为fwd
setTabAt(tab, i, fwd);
// advance设置为true,保证可以进入到while循环,对i进行--操作
advance = true;
}
else if (f instanceof TreeBin) {
//红黑树迁移省略
}
}
}
}
}
}
3.3 helpTransfer方法-协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 判断新老数组都不为空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 基于老数组长度计算扩容戳
int rs = resizeStamp(tab.length);
// fwd中的新数组,和当前正在扩容的新数组是否相等。相等:可以协助扩容。不相等:要么扩容结束,要么开启了新的扩容
while (nextTab == nextTable
// 老数组是否改变了。 相等:可以协助扩容。不相等:扩容结束了
&& table == tab
// 如果正在扩容,sizeCtl肯定为负数,并且给sc赋值
&& (sc = sizeCtl) < 0) {
// 将sc右移16位,判断是否与扩容戳一致。 如果不一致,说明扩容长度不一样,退出协助扩容
if ((sc >>> RESIZE_STAMP_SHIFT) != rs
// 这两个是bug 要右移 16位判断才成立
|| sc == rs + 1
|| sc == rs + MAX_RESIZERS
// transferIndex是从高索引位置到低索引位置领取数据的一个核心属性(transfer 当中计算)
// 如果满足 小于等于0,说明任务被领光了。
|| transferIndex <= 0)
break;
// CAS 修改线程进入协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
四、作者的话
这玩意解读起来也太累了吧,人要疯掉!!!!!