当数组中存放的节点个数超过了阈值就可以任务当前的哈希冲突可能性较大,HashMap就会对当前数组进行扩容操作。在普通的HashMap中就会直接声明一个两倍大小的新数组,然后从旧数组中从左到右进行转化,但是在多线程下肯定会存在线程安全的问题。那么ConcurrentHashMap是如何来保证多线程下的扩容安全的呢?
transfer是进行扩容操作的方法,接收两个参数tab
(原容器数组对象),nextTab
(新容器数组对象,一般长度为tab的两倍)。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride : 单个线程需要处理的下标数
// 当有多个CPU参与扩容时,每个CPU处理的数量就是(n / 8 / NPU)
// 如果单个CPU处理的数量小于MIN_TRANSFER_STRIDE(16),就取最小值MIN_TRANSFER_STRIDE
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 当扩容刚开始时nextTab为null,进行初始化操作
if (nextTab == null) { // initiating
// 创建一个新的2倍长度的心数组
try {
@SuppressWarnings("unchecked")
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 = nextTab;
transferIndex = n;
}
// 扩容后数组长度
int nextn = nextTab.length;
// 定义ForwardingNode类并设置状态为MOVED,该类指向新的扩容数组
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 标识是否需要
boolean advance = true;
// 标志扩容是否结束
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 循环查找需要进行处理的下标
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;
}
}
// 表示当前所需处理的数组下标不合法,可能已经完成了扩容操作
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 已完成扩容,将用户访问的数组指向新数组,并重新设置阈值
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 在完成后将sizeCtl - 1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 代表其他扩容执行的线程都已经执行完成,
// 符合就将扩容状态标志置为true
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
// 重置i等于原数组长度 重新检查所有的内容
i = n; // recheck before commit
}
}
// 如果i的位置为null,直接将null放入fwd中
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 如果MOVED说明已经处理过了
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) {
int runBit = fh & n;
Node<K,V> lastRun = f;
// 先将最后一部分进行划分
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;
// 利用hash值通过头插法生成左右两个链表
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);
setTabAt(tab, i, fwd);
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;
}
}
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(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
扩容流程
首次开启扩容任务的线程:
-
调用transfer方法,其中nextTab参数为null;
-
根据CPU数及当前线程数,计算出每个线程所要负责的数组区间长度值stride(最小值为MIN_TRANSFER_STRIDE,16);
-
创建一个数组长度为原来两倍的新数组,将新数组放入nextTable变量中,并设置transferIndex为原数组长度值(转换由原数组从右往左执行);
3.1 由于新数组长度可能过大导致出现OOM异常,说明已达到最大长度,直接将阈值设置为int的最大值,停止后续的扩容操作;
-
根据当前未转化区间的最大值以及stride,计算出转化的起始下标值和终止下标值;
-
按照起始下标值进行循环遍历转换;
-
创建ForwardingNode对象fwd,参数为新数组,同时节点的hash值为MOVED;
-
当前下标值非法;
6.1 如果finishing为true,代表整个扩容操作已完成,将table指向新数组,nextTab设置为null,阈值sizeCtl为原数组长度的1.75倍;
6.2 如果finishing为false,代表当前线程的扩容任务已完成,并且已无可分配的区间,将sizeCtl进行减1;
6.2.2 当(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 为true,代表还有其他线程正在进行扩容操作,直接结束当前方法;
6.2.3 反之为false,说明当前是完成扩容任务的最后一个线程,将finishing置为true,并完成后续的检查以及数组替换任务;
-
当前下标节点对象为null,通过cas操作将fwd对象放入该下标处;
-
当前下标节点的hash为MOVED,代表当前位置已完成转换,进行下一轮循环;
-
对当前下标节点对象加锁;
10.1 如果对应节点对象为改变,按照HashMap的扩容思想(根据最高位hash值,分为左右两部分)进行扩容。
其他线程对扩容时的操作:
putVal
-
获取到对应的下标节点对象;
-
如果节点对象为null,直接进行将新的节点放入;
-
如果节点的hash值为MOVED代表该下标节点已完成扩容转化任务,帮助其他线程完成剩余扩容任务;
3.1 获取到节点中存放的新数组对象,将sizeCtl的值+1,然后按照区间分配的原则进行扩容任务;
-
如果不满足上面两种情况,按照正常的插入操作进行。
get
- 返回null
1. ConcurrentHashMap如何保证扩容安全?
-
cas操作保证只有一个线程进行新数组的初始化工作;
-
将整个数组分为若干个区间,每个区间都只对应单独的线程;
-
转化前在当前下标节点上加synchronized锁;
-
插入操作检测到对应的节点处于MOVED状态,先完成扩容在进行插入。
2. 如何判断扩容任务完成?
扩容任务刚开始时设置sizeCtl为 (rs << RESIZE_STAMP_SHIFT) + 2),后续有新的线程加入参与扩容任务就将sizeCtl + 1,线程当前分配的区间已转化完成且没有可以分配的区间,此时将sizeCtl - 1。如果最后sizeCtl 等于 (rs << RESIZE_STAMP_SHIFT) + 2) 说明扩容完成。