三种触发方式
- 当前容量超过阈值
- 当链表中元素个数超过默认设定(8个),当数组的大小还未超过64的时候,此时进行数组的扩容
- 当发现其他线程扩容时,协助扩容
1. tryPreSize方法-初始化数组
// 扩容前操作,putAll,链表转红黑树 插入map的长度(putAll)
private final void tryPresize(int size) {
// 这个判断是给putAll留的,要计算当前数组的长度(初始化)
// 如果size大于最大长度 / 2,直接将数组长度设置为最大值。
// tableSizeFor,将长度设置的2的n次幂
// c是初始化数组长度
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
// sc是给sizeCtl赋值
// -1:正在初始化数组,小于-1:正在扩容,0:代表还没初始化数组,大于0:可能初始化了(代表阈值),也可能没初始化(初始化的长度)
int sc;
while ((sc = sizeCtl) >= 0) {
// 代表没有正在执行初始化,也没有正在执行扩容。、
// tab:数组,n:数组长度
Node<K,V>[] tab = table; int n;
// 判断数组是不是还没初始化呢
if (tab == null || (n = tab.length) == 0) {
// 初始化数组,和initTable一样的东西
// 在sc和c之间选择最大值,作为数组的初始化长度
n = (sc > c) ? sc : c;
// 要初始化,就直接把sizeCtl设置为-1,代表我要初始化数组
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// DCL!
if (table == tab) {
// 创建数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 初始化数组赋值给成员变量
table = nt;
// sc先设置成阈值
sc = n - (n >>> 2);
}
} finally {
// 将sc赋值给sizeCtl
sizeCtl = sc;
}
}
}
// 要么是c没有超过阈值,要么是超过最大值,啥事不做~~~
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 省略部分代码。
}
}
2. tryPreSize方法-扩容标识戳
// 扩容前操作
private final void tryPresize(int size) {
while ((sc = sizeCtl) >= 0) {
// 省略部分初始化代码
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
// 扩容前操作!
else if (tab == table) {
// 计算扩容标识戳(基于老数组长度计算扩容标识戳,因为ConcurrentHashMap允许多线程迁移数据。)
int rs = resizeStamp(n);
// 这里是一个BUG,当前sc在while循环中,除了初始化没有额外赋值的前提下,这个sc < 0 永远进不来。
// 虽然是BUG,但是清楚sc < 0 代表正在扩容
if (sc < 0) {
Node<K,V>[] nt; 31 ~ 16 15 ~ 0
// 这里是第二个BUG
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || // 判断协助扩容线程的标识戳是否一致
sc == rs << RESIZE_STAMP_SHIFT + 1 || // BUG之一,在判断扩容操作是否已经到了最后的检查阶段
sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS || // BUG之一,判断扩容线程是否已经达到最大值
(nt = nextTable) == null || // 新数组为null,说明也已经扩容完毕,扩容完毕后,才会把nextTable置位null
transferIndex <= 0) // transferIndex为线程领取任务的最大节点,如果为0,代表所有老数据迁移任务都没领干净了
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 还没有执行扩容,当前线程可能是第一个进来执行扩容的线程
// 基于CAS的方式,将sizeCtl从原值改为 扩容标识戳左移16位
// 10000000 00011010 00000000 00000010 一定是< -1的负数,可以代表当前ConcurrentHashMap正在扩容
// 为什么是低位+2,代表1个线程扩容。 低位为5,就代表4个线程正在并发扩容
// 扩容分为2部:创建新数组,迁移数据。
// 当最后一个线程迁移完毕数据后,对低位-1.最终结果低位还是1,需要对整个老数组再次检查,数据是否迁移干净
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 开始扩容操作,传入老数组~~
transfer(tab, null);
}
}
}
static final int resizeStamp(int n) {
// 32~64
// 00000000 00000000 00000000 00011010
// 计算n在二进制表示时,前面有多少个0
// 00000000 00000000 10000000 00000000
// 00000000 00000000 10000000 00011010
// 前面的操作是基于数组长度等到一个标识,方便其他线程参与扩容
// 后面的值是为了保证当前扩容戳左移16位之后,一定是一个负数
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
三、transfer方法-构建新数组
transfer方法:
- 计算步长
- 初始化新数组
- 线程领取迁移数据任务
- 判断迁移是否完成,并判断当前线程是否是最后一个完成的
- 查看当前位置数据是否为null
- 查看当前位置数据是否为fwd
- 链表迁移数据-lastRun机制
- 红黑树迁移-迁移完数据长度小于等于6,转回链表
// 扩容操作,以第一个进来执行扩容的线程为例。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 创建新数组流程!
// n:老数组长度32, stride:扩容的步长16
int n = tab.length, stride;
// NCPU:4
// 00000000 00000000 00000000 00000000
// 00000000 00000000 00000100 00000000 - 1024 512 256 128 / 4 = 32
// 如果每个线程迁移的长度基于CPU计算,大于16,就采用计算的值,如果小于16,就用16
// 每个线程每次最小迁移16长度数据
// stride = 1 < 16
// 这个操作就是为了充分发挥CPU性能,因为迁移数据是CPU密集型操作,尽量让并发扩容线程数量不要太大,从而造成CPU的性能都消耗在了切换上,造成扩容效率降低
// 如果要做优化的,推荐将扩容线程数设置为和CPU内核数+1一致。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 如果新数组没有初始化
if (nextTab == null) {
try {
// 初始化数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
// 新数组赋值给nextTab
nextTab = nt;
} catch (Throwable ex) {
// 要么OOM,要么数组长度达到最大值。
sizeCtl = Integer.MAX_VALUE;
return;
}
// 将nextTable成员变量赋值
nextTable = nextTab;
// transferIndex设置为老数组长度
transferIndex = n;
}
}
// n:老数组长度
// stride:步长
// nextTale,nextTab:新数组
// transferIndex:线程领取任务时的核心属性
四、transfer方法-迁移数据
第一步,线程领取迁移数据的任务
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 省略部分代码
// n:老数组长度 32
// stride:步长 16
// nextTale,nextTab:新数组
// nextn:新数组长度 64
// transferIndex:线程领取任务时的核心属性 32
// 先看领取任务的过程!!!
// 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 领任务的核心标识
boolean advance = true;
// 扩容结束了咩?
boolean finishing = false;
// 扩容的for循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 领取任务的while循环
while (advance) {
int nextIndex, nextBound;
// 第一个判断是为了迁移下一个索引数据(暂时不管)
if (--i >= bound || finishing)
advance = false;
// 说明没有任务可以领取了(暂时不管)
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// transferIndex:16
// stride:16,nextIndex:32,nextBound:16
// bound:16,i:31
// 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
第二步:判断是否结束,以及线程退出扩容,并且为空时,设置fwd,并且hash为moved直接移动到下个位置
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 省略部分代码
// n:老数组长度 32
// stride:步长 16
// nextTale,nextTab:新数组
// nextn:新数组长度 64
// transferIndex:线程领取任务时的核心属性 32
// 先看领取任务的过程!!!
// 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 领任务的核心标识
boolean advance = true;
// 扩容结束了咩?
boolean finishing = false;
// 扩容的for循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 领取任务的while循环
while (advance) {
int nextIndex, nextBound;
// 第一个判断是为了迁移下一个索引数据(暂时不管)
if (--i >= bound || finishing)
advance = false;
// 说明没有任务可以领取了(暂时不管)
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// transferIndex:16
// stride:16,nextIndex:32,nextBound:16
// bound:16,i:31
// 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 迁移最后一段的线程干完活了,或者其他线程没有任务可以领取了。
if (i < 0) {
int sc;
// 判断结束了没,第一次肯定进不来
if (finishing) {
// 结束扩容,将nextTabl设置为null
nextTable = null;
// 将迁移完数据的新数组,指向指向的老数组
table = nextTab;
// 将sizeCtl复制为下次扩容的阈值
sizeCtl = (n << 1) - (n >>> 1);
// 结束
return;
}
// 到这,说明当前线程没有任务可以领取了
// 基于CAS的方式,将低位-1,代表当前线程退出扩容操作(如果是最后一个,还有一个额外的活)
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断我是否是最后一个完成迁移数据的线程,如果不是,直接return结束
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果到这,说明我是最后一个结束迁移数据的线程。
// finishing结束表示和advance领取任务的标识全部设置为true
finishing = advance = true;
// i设置为老数组长度,从头到位再检查一次整个老数组。
i = n;
}
/*
额外分析:当前线程完成领取的迁移任务后,再次进入while循环,
查看是否有任务可以领取,如果transferIndex变为0了,代表我没有任务可以领取,
将i设置为-1没有任务可以领取,退出当前扩容操作:
1、基于CAS将sizeCtl - 1代表我退出扩容操作
2、-1成功后,还要判断,我是不是最后一个退出扩容的线程(sc - 2值是否是 扩容标识戳 << 16)如果不是,直接return结束
3、如果是最后一个结束迁移的线程,将i复制为老数组长度,重新从末位到头部再次检查一圈
*/
}
else if ((f = tabAt(tab, i)) == null)
// 如果发现迁移为主的数据为null,设置放置一个fwd,代表当前位置迁移完成
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
// 是在检查时的逻辑
advance = true;
五、transfer方法-lastRun机制
就是迁移链表到新数组时的操作
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 省略部分代码
// n:老数组长度 32
// stride:步长 16
// nextTale,nextTab:新数组
// nextn:新数组长度 64
// transferIndex:线程领取任务时的核心属性 32
// 先看领取任务的过程!!!
// 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 领任务的核心标识
boolean advance = true;
// 扩容结束了咩?
boolean finishing = false;
// 扩容的for循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 领取任务的while循环
while (advance) {
int nextIndex, nextBound;
// 第一个判断是为了迁移下一个索引数据(暂时不管)
if (--i >= bound || finishing)
advance = false;
// 说明没有任务可以领取了(暂时不管)
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// transferIndex:16
// stride:16,nextIndex:32,nextBound:16
// bound:16,i:31
// 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 迁移最后一段的线程干完活了,或者其他线程没有任务可以领取了。
if (i < 0) {
int sc;
// 判断结束了没,第一次肯定进不来
if (finishing) {
// 结束扩容,将nextTabl设置为null
nextTable = null;
// 将迁移完数据的新数组,指向指向的老数组
table = nextTab;
// 将sizeCtl复制为下次扩容的阈值
sizeCtl = (n << 1) - (n >>> 1);
// 结束
return;
}
// 到这,说明当前线程没有任务可以领取了
// 基于CAS的方式,将低位-1,代表当前线程退出扩容操作(如果是最后一个,还有一个额外的活)
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断我是否是最后一个完成迁移数据的线程,如果不是,直接return结束
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果到这,说明我是最后一个结束迁移数据的线程。
// finishing结束表示和advance领取任务的标识全部设置为true
finishing = advance = true;
// i设置为老数组长度,从头到位再检查一次整个老数组。
i = n;
}
/*
额外分析:当前线程完成领取的迁移任务后,再次进入while循环,
查看是否有任务可以领取
如果transferIndex变为0了,代表我没有任务可以领取,
将i设置为-1没有任务可以领取,退出当前扩容操作:
1、基于CAS将sizeCtl - 1代表我退出扩容操作
2、-1成功后,还要判断,我是不是最后一个退出扩容的线程(sc - 2值是否是 扩容标识戳 << 16)如果不是,直接return结束
3、如果是最后一个结束迁移的线程,将i复制为老数组长度,重新从末位到头部再次检查一圈
*/
}
else if ((f = tabAt(tab, i)) == null)
// 如果发现迁移为主的数据为null,设置放置一个fwd,代表当前位置迁移完成
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
// 是在检查时的逻辑
advance = true;
else {
// 迁移数据,加锁!
synchronized (f) {
// 拿到当前位置数据
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 说明当前节点状态正常,不是迁移,不是红黑树,不是预留
if (fh >= 0) {
// fh与老数组进行&运算,得到runBit
// 00001111
// 00010000
// 这个计算的结果,会决定当前数据在迁移时,是放到新数组的i位置还有新数组的 i + n位置
int runBit = fh & n;
Node<K,V> lastRun = f;
// lastRun机制
// 提前循环一次链表,将节点赋值到对应的高低位Node./
// 如果链表最后面的值没有变化,那就不动指针,直接复制。
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位置,不再继续往下循环
// 这样可以不用每个节点都new,避免GC和OOM问题。
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);
// 将当前迁移完的桶位置,设置上fwd,代表数据迁移完毕
setTabAt(tab, i, fwd);
// advance,代表执行下次循环,i--。
advance = true;
}
// 省略红黑树迁移!
}
}
}
}
}
六、helpTransfer方法-协助扩容
// 协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 老数组不为null,当前节点是fwd,新数组不为null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 创建自己的扩容标识戳
int rs = resizeStamp(tab.length);
// 判断之前赋值的内容是否有变化,并且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;
// CAS,将sizeCtl + 1,代表来协助扩容了
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}