总结:
private transient volatile int sizeCtl;
多线程之间,以volatile的方式读取sizeCtl属性,来判断ConcurrentHashMap当前所处的状态。
通过cas设置sizeCtl属性,告知其他线程ConcurrentHashMap的状态变更。
不同状态,sizeCtl所代表的含义也有所不同:
未初始化:
sizeCtl = 0:表示没有指定初始容量
sizeCtl > 0:表示初始容量
初始化中:
sizeCtl=-1,标记作用,告知其他线程,正在初始化
正常状态:
sizeCtl = 0.75n ,扩容阈值
扩容中:
sizeCtl < 0 : 表示有其他线程正在执行扩容
sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此时只有一个线程在执行扩容
sizeCtl = -N 表示,这个高16位表示当前扩容的标志,每次扩容都会生成一个不一样的标志,低16位表示参与扩容的线程数量
讲扩容 transfer(tab, null);
先讲一下扩容的总体思路:
扩容的时候 会算出一个步长 根据cpu的核心数量来算,最小为16 现在为了方便 假设 数组数量为4 步长为2 那么就会通过计算算出A线程的步长是2开始位置是下标是3 2 默认是从数组从右像左扩容的 B线程来了算出的位置就是1 0 然后把对应位置转移过去,并且会把对应位置更新为一个forwardingNode 他的hash值为1 这也是put操作中判断正在扩容并且帮助扩容的重要标志。transferIndex=4
————————————————
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//这里就是算步长,根据cpu核心数 最小为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
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<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;//这个变量代表了当前线程是否需要继续扩容
//假设当然map容量为4 步长为1 只要A线程扩容每次扩容一个 那么下一次肯定是要继续扩容的
//但是也有可能有多个线程同时扩容 我扩容完了 其他线程把整个也扩容完了就不需要前进,继续扩容了后面会修改为false
//判断当前线程的工作是否完成 跟前面一样 假设只要A线程再扩容 那么我肯定还需要再继续工作,
//但是如果其他线程正在扩容其他的 我就完成了 就修改完true
boolean finishing = false; // to ensure sweep before committing nextTab
//这里的I和bound带变了 每次扩容的边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//如果i减一以后还是>=bound说明还在步长内不需要继续前进扩容
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
//下一个边界都小于=0了说明也不继续前进了
i = -1;
advance = false;
}
//第一次回进入到这里 假设map容量为4 步长为2
//TRANSFERINDEX再前面看到是等于n=4的
//nextIndex=TRANSFERINDEX=4
//nextBound=4-2=2;
//i=nextIndex=1=3
//同时cas修改 TRANSFERINDEX为nextBound
//现在明白了把 就是这样算的
//假设其他线程也修改 他们获得的TRANSFERINDEX是2 注意看上面的else if 这时候
//来了一个线程B获取到的TRANSFERINDEX就是2 算出的i和bound就是1 和0
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 是否等于最开始那个初始值 等于初始值代表所有线程扩容完毕
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
}
}
//如果说当前位置为空 说明不需要转移 之间设置fwd 防止其他线程这时候插入新值
//扩容的时候肯定不能插入嘛
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//跟null一样的道理
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;
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;
}
}
}
}
}
}
扩容流程:
1、根据操作系统的 CPU 核数和集合 length 计算每个核一轮处理桶的个数,最小是16
2、修改 transferIndex 标志位,每个线程领取完任务就减去多少,
比如初始大小是transferIndex = table.length = 64,每个线程领取的桶个数是16,
第一个线程领取完任务后transferIndex = 48,也就是说第二个线程这时进来是从第 48 个桶开始处理,再减去16,依次类推,这就是多线程协作处理的原理
3、领取完任务之后就开始处理,如果桶为空就设置为 ForwardingNode ,
如果不为空就加锁拷贝,只有这里用到了 synchronized 关键字来加锁,为了防止拷贝的过程有其他线程在put元素进来。
拷贝完成之后也设置为 ForwardingNode节点。
4、如果某个线程分配的桶处理完了之后,再去申请,发现 transferIndex = 0,
这个时候就说明所有的桶都领取完了,但是别的线程领取任务之后有没有处理完并不知道,
该线程会将 sizeCtl 的值减1,然后判断是不是所有线程都退出了,如果还有线程在处理,就退出,
直到最后一个线程处理完,发现 sizeCtl = rs<< RESIZE_STAMP_SHIFT 也就是标识符左移 16 位,
才会将旧数组干掉,用新数组覆盖,并且会重新设置 sizeCtl 为新数组的扩容点。
以上过程总的来说分成两个部分:
1、分配任务:这部分其实很简单,就是把一个大的数组给切分,切分多个小份,然后每个线程处理其中每一小份,
当然可能就只有1个或者几个线程在扩容,那就一轮一轮的处理,一轮处理一份。
2、处理任务:复制部分主要有两点,第一点就是加锁,第二点就是处理完之后置为ForwardingNode来占位标识这个位置被迁移过了。
ForwardingNode用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点。
————————————————
版权声明:本文为优快云博主「Hanyinh」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_43871678/article/details/115966911