简介
ConcurrenHashMap 在扩容过程中主要使用 sizeCtl 和 transferIndex 这两个属性来协调多线程之间的并发操作,并且在扩容过程中大部分数据依旧可以做到访问不阻塞 。
sizeCtl 属性在各个阶段的作用
(1)、新建而未初始化时
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
用处:sizeCtl 用于记录初始容量大小,仅用于记录集合在实际创建时应该使用的大小的作用 。
(2)、初始化过程中
U.compareAndSwapInt(this, SIZECTL, sc, -1)
用处:将 sizeCtl 值设置为 -1 表示集合正在初始化中,其他线程发现该值为 -1 时会让出CPU资源以便初始化操作尽快完成 。
(3)、初始化完成后
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
sizeCtl = sc;
用处:sizeCtl 用于记录当前集合的负载容量值,也就是阈值(最大长度*0.75)。
(4)、正在扩容时
//第一条扩容线程设置的某个特定基数
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
//后续线程加入扩容大军时每次加 1
U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
//线程扩容完毕退出扩容操作时每次减 1
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
用处:sizeCtl 用于记录当前扩容并发线程数情况,此时 sizeCtl 的值为:((rs << RESIZE_STAMP_SHIFT) + 2) + (正在扩容的线程数) ,并且该状态下 sizeCtl < 0 。
什么时候会扩容?
当往hashMap中成功插入一个key/value节点时,有两种情况可能触发扩容动作:
1、如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。
2、调用put方法新增节点时,在结尾会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。
3、扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点会触发扩容 。
4、putAll 批量插入或者插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容 。
注意:桶上链表长度达到 8 个或者以上,并且数组长度为 64 以下时只会触发扩容而不会将链表转为红黑树 。
transfer()扩容方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride; //stride 主要和CPU相关
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小16个。
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容到2倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE; //扩容保护
return;
}
nextTable = nextTab;
transferIndex = n; //扩容总进度,>=transferIndex的桶都已分配出去。
}
int nextn = nextTab.length;
//扩容时的特殊节点,标明此节点正在进行迁移,扩容期间的元素查找要调用其find()方法在nextTable中查找元素。
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的值确定当前范围内下一个要处理的节点。
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) //每次循环都检查结束条件
advance = false;
//迁移总进度<=0,表示所有桶都已迁移完成。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) { //transferIndex减去已分配出去的桶。
//确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//当前线程自己的活已经做完或所有线程的活都已做完,第二与第三个条件应该是下面让"i = n"后,再次进入循环时要做的边界检查。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { //所有线程已干完活,最后才走这里。
nextTable = null;
table = nextTab; //替换新table
sizeCtl = (n << 1) - (n >>> 1); //调sizeCtl为新容量0.75倍。
return;
}
//当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-1。
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//还记得addCount()处给sizeCtl赋的初值吗?相等时说明没有线程在参与扩容了,置finishing=advance=true,为保险让i=n再检查一次。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd); //如果i处是ForwardingNode表示第i个桶已经有线程在负责迁移了。
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) { //桶内元素迁移需要加锁。
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//链表的处理
if (fh >= 0) { //>=0表示是链表结点
//由于n是2的幂次方(所有二进制位中只有一个1),如n=16(0001 0000),第4位为1,那么hash&n后的值第4位只能为0或1。所以可以根据hash&n的结果将所有结点分为两部分。
int runBit = fh & n;
Node<K,V> lastRun = f;
//找出最后一段完整的fh&n不变的链表,这样最后这一段链表就不用重新创建新结点了。
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之前的结点因为fh&n不确定,所以全部需要重新迁移。
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);
}
//低位链表放在i处
setTabAt(nextTab, i, ln);
//高位链表放在i+n处
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); //在原table中设置ForwardingNode节点以提示该桶扩容完成。
advance = true;
}
//红黑树的处理
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;
}
}
//如果拆分后的树的节点数量已经少于6个就需要重新转化为链表
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;
//CAS存储在nextTable的i位置上
setTabAt(nextTab, i, ln);
//CAS存储在nextTable的i+n位置上
setTabAt(nextTab, i + n, hn);
//CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
源码很长,我们分开解读,第一部分:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride; //stride 主要和CPU相关
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小步长16个。
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容到2倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE; //扩容保护
return;
}
nextTable = nextTab;
transferIndex = n; //扩容总进度,>=transferIndex的桶都已分配出去。
int nextn = nextTab.length;
}
-
n为扩容之前数组的长度,stride 主要和CPU相关,含义为步长,每个线程在扩容时拿到的长度,最小为16。
-
nextTab为新的table,如果nextTab为空就新建一个table,大小为原来的2倍,代表双倍扩容。
-
transferIndex=n;n为扩容前的大小。
-
int nextn = nextTab.length; nextn为扩容后数组的大小
//构造一个ForwardingNode用于多线程之间的共同扩容情况
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //继续遍历的确认标志
boolean finishing = false; //所有桶是否都已迁移完成标志
-
fwd是一个标志,标明此节点正在进行迁移。当其他线程进行操作的时候发现这个位置存放的是fwd就知道正在进行扩容。、
-
advance是遍历的确认标志,是否再往前进行遍历。finishing 是所有桶是否都已迁移完成标志
//遍历每个节点
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh; //定义一个节点和一个节点状态判断标志fh
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) 每次循环都检查结束条件
advance = false;
//迁移总进度<=0,表示所有桶都已迁移完成
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
-
接下来开始遍历每一个节点,bound是当前步长结尾的位置。初始化一个节点f和一个节点状态判断标志fh
-
while循环的作用是确定当前线程要迁移的桶的范围或通过更新i的值确定当前范围内下一个要处理的节点。
-
当前步长内元素转移完成后 i = -1
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) { //transferIndex减去已分配出去的桶。
//确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
这里是一个CAS的计算来修改TRANSFERINDEX(转移到的下标),配合上面一段代码计算出当前线程操作数组的具体区域。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果原table已经复制结束
if (finishing) {
nextTable = null; //可以看出在扩容的时候nextTable只是类似于一个temp用完会丢掉
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //修改扩容后的阀值,应该是现在容量的0.75倍
return;//结束循环
}
//采用CAS算法更新SizeCtl。
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 < 0 || i >= n || i + n >= nextn一共三个条件。1.当前线程自己的活已经做完或所有线程的活都已做完,第二与第三个条件应该是下面让"i = n"后,再次进入循环时要做的边界检查。
-
如果当前线程的工作做完后发现此时已经finialing了,就可以把sizeCtl 改为新的值结束循环。
//CAS算法获取数组第i的节点,为空就设为forwordingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果这个节点的hash值是MOVED,就表示这个节点是forwordingNode节点,就表示这个节点已经被处理过了,直接跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
如注释所示,CAS算法获取数组第i个位置的值,为空就设为fwd标志位。如果这个节点的hash值是MOVED,代表内容已经是fwd,被其他线程处理过了,直接跳过,继续前进。
-
这里先根据 fh 判断头的位置是链表的头节点还是树的根节点,如果是链表的话就执行链表转移,在转移过程中使用的是CAS算法。使用头插法进行转移。
-
如果是红黑树的话转移的方法和HashMap1.8中对红黑树的转移是一样的,是使用了两个链表,一个是高位链表一个是低位链表:
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
//lo 为低位链表头结点,loTail 为低位链表尾结点,hi 和 hiTail 为高位链表头尾结点
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//同样也是使用高位和低位两条链表进行迁移
//使用for循环以链表方式遍历整棵红黑树,使用尾插法拼接 ln 和 hn 链表
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
//这里面形成的是以 TreeNode 为节点的链表
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;
}
}
//形成中间链表后会先判断是否需要转换为红黑树:
//1、如果符合条件则直接将 TreeNode 链表转为红黑树,再设置到新数组中去
//2、如果不符合条件则将 TreeNode 转换为普通的 Node 节点,再将该普通链表设置到新数组中去
//(hc != 0) ? new TreeBin<K,V>(lo) : t 这行代码的用意在于,如果原来的红黑树没有被拆分成两份,那么迁移后它依旧是红黑树,可以直接使用原来的 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;
//setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
//使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
//使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
setTabAt(nextTab, i, ln);
//使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
setTabAt(nextTab, i + n, hn);
//迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
setTabAt(tab, i, fwd);
//advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
advance = true;
}
helpTransfer()
这个方法是帮助其他线程进行扩容。添加、删除节点之处都会检测到table的第i个桶是ForwardingNode的话会调用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);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
tryPresize()
putAll批量插入或者插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会调用到这个方法.
private final void tryPresize(int size) {
//根据传入的size计算出真正的新容量,因为新容量需要是2的幂次方。
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;
//如果数组没有初始化则进行初始化,这个选项主要是为批量插入操作方法 putAll 提供的
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c; //table未初始化则给一个初始容量
//初始化时将 sizeCtl 设置为 -1
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
//初始化完成后 sizeCtl 用于记录当前集合的负载容量值,也就是触发集合扩容的阈值
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会进入到下面这个 else if 分支
else if (tab == table) {
int rs = resizeStamp(n);
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
扩容过程图解
触发扩容的操作:
假设目前数组长度为8,数组的元素的个数为5。再放入一个元素就会触发扩容操作。
总结一下扩容条件:
(1) 元素个数达到扩容阈值。
(2) 调用 putAll 方法,但目前容量不足以存放所有元素时。
(3) 某条链表长度达到8,但数组长度却小于64时
CPU核数与迁移任务hash桶数量分配(步长)的关系
单线程下线程的任务分配与迁移操作
多线程如何分配任务?
普通链表如何迁移?
红黑树如何迁移?
hash桶迁移中以及迁移后如何处理存取请求?