简介:
因为博主前面写过HashMap
的解析,所以这里只分析和HashMap
不同点,类似的地方会略过,有需要的朋友可以结合HashMap
的文章一起看。
不容易,终于把这篇文章更新出来了,ConcurrentHashMap
实在是太难了,终于熬完了这锅鸡汤。one day day的,现在几乎可以背出来了。
废话不多说,跳过前戏,直接看代码(提醒一下,精华都是各种if判断
)。
先列几个重要的参数:
//阈值
//=0:默认值
//-1:表示正在初始化
//<-1:表示多个线程在扩容(-2是第一个扩容线程,后面每多一个就+1)
//低16位是扩容线程数量+1,因为第一个扩容线程是+2,高16位的最高位是1,剩下15位是数组的长度)
//>1:表示需要扩容阈值
private transient volatile int sizeCtl;
//扩容时线程每次领取完还剩余的任务
private transient volatile int transferIndex;
//标志数组的下标正在扩容
static final int MOVED = -1;
//标志数组的下标存放的是树结构
static final int TREEBIN = -2;
方法列表
putVal第一部分
//跟hashMap不一样的地方,不允许key为null
if (key == null || value == null) throw new NullPointerException();
//计算hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//初始化
tab = initTable();
//计算下标,跟hashMap一样
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//跟hashMap不一样的地方,CAS新增头节点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
第一部分还是比较简单的:
- 计算hash
- 没有初始化就先初始化
- 根据hash算出下标,如果下标为null,就用CAS新增
spread():
这里的计算下标跟hashMap
有点不一样,就是这里不允许hash
为负数。
//跟hashmap有点区别,不仅高16参与了计算,并且还再次 & 计算
return (h ^ (h >>> 16)) & HASH_BITS;
简单来说就是避免hash值是负数,具体原因下篇文章分析。
initTable():
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl刚开始是0,sizeCtl<0,说明有别的线程CAS成功了
if ((sc = sizeCtl) < 0)
//所以当前线程就先让出CPU时间片,等待下次的系统调度,再次走上面的while判断
Thread.yield(); // lost initialization race; just spin
//用CAS去修改SIZECTL为-1,表示在初始化,扩容是-2
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//双重校验
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
//sizeCtl>0,就是扩容阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
initTable
的作用比较简单,就是初始化table
的,但是要避免多线程初始化。
关于这里的双重校验
额外说明一下:
仔细想想,这个双重校验不是为了这里,因为CAS是立即得到返回结果的,不是Lock或synchronized,不会阻塞
即便是多线程同时initTable也不会出现问题,这里是为了防止putAll的
在没有初始化的时候,一个线程put,一个线程putAll,就会出现问题。
putAll的初始化是调的tryPresize,这里的二次校验是防止tryPresize也在初始化
putVal第二部分
//判断hash是否为MOVED,就是判断是否有线程在扩容
else if ((fh = f.hash) == MOVED)
//帮助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//跟hashMap不一样的地方,如果此下标已经有值了,锁住头节点
synchronized (f) {
//获取该下标的值,用的getObjectVolatile,强制从主存中获取属性值
if (tabAt(tab, i) == f) {
//判断是不是链表
if (fh >= 0) {
binCount = 1;
//遍历链表,尾插
for (Node<K,V> e = f;; ++binCount) {
//...省略掉
}
//判断是红黑树结构
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//...省略掉
}
}
}
//binCount初始是0,0代表插入的下标没值,插入的是头节点
//binCount!= 0,1代表插入的是链表,2代表是树
//所以先判断binCount!= 0,再去判断是否达到转树的阈值
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//链表转树
treeifyBin(tab, i);
if (oldVal != null)
//说明是替换值,直接return了,下面的扩容不需要判断了
return oldVal;
break;
}
}
}
//数量+1,并且判断是否需要扩容
addCount(1L, binCount);
return null;
}
这里就是精华了:
- 判断是否正在扩容,是的话就帮助扩容
- 插入链表或者
Tree
里面去 - 判断是否需要链表转
Tree
,或者是否需要替换旧值(treeifyBin) - 总数量+1,且判断是否需要扩容(addCount)
treeifyBin():
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//数量不够,先扩容,不转树
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
//这里遍历链表,把Node转成TreeNode,跟hashMap一样
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//将新的TreeNode改成Tree结构
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
treeifyBin(Node<K,V>[] tab, int index)
的作用:
- 判断总数是否小于64,小于就先扩容,不转Tree
- 遍历链表,把Node转成TreeNode
- 把TreeNode改成Tree结构
tryPresize():
private final void tryPresize(int size) {
//判断是否大于最大容量的一半,是的话就取最大容量,否则就取扩容2倍之后的最小的2的幂次方
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//判断是不是要初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
//用CAS去修改SIZECTL为-1,表示在初始化,扩容是-2
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//二次校验,同样,校验的不是这里,是initTable那边的初始化
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
//这里是判断是否需要扩容
//c <= sc:判断的是putAll丢进来的集合数量是否小于扩容阈值
//n >= MAXIMUM_CAPACITY:判断的是数组长度是否大于最大容量
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//走到这,说明要扩容
else if (tab == table) {
//这里的判断跟addCount方法的判断一致,在addCount会详细说明
int rs = resizeStamp(n);
//省略掉
.....
}
}
}
tryPresize(int size):
其实还挺简单的,判断要初始化还是扩容,扩容的判断细节留到addCount
里面详细讲解。
TreeBin():
TreeBin(TreeNode<K,V> b) {
//注意这一步,把Node的hash改成TREEBIN,也就是是-2,所以判断 < 0就知道是Tree结构了
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
//红黑树的新增,跟hashMap,感兴趣的朋友可以去看博主写的hashMap篇
for (TreeNode<K,V> x = b, next; x != null; x = next) {
//省略掉
.....
}
this.root = r;
assert checkInvariants(root);
}
TreeBin(TreeNode<K,V> b):
也很简单,比起hashMap,就多了一步,把Node
的hash
改成-2
用来区分链表和红黑树的。
addCount():
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//判断CAS是否失败
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//省略掉
.....
}
//判断需不需要扩容
if (check >= 0) {
//省略掉
.....
}
}
也是相当复杂的,一步步看,先看下大概思路:
- 先判断有没有线程CAS增加总数量失败的,或者说自己CAS失败了,有就去记录
- 判断需不需要扩容
好,简简单单的2步,但是复杂的令人想吐。
判断CAS是否失败:
这部分就留到下篇文章(细节篇)里面去分析,因为不是很重要。这里就不影响大家的思路了,接着看高潮部分,扩容(下篇文章跟这篇会同步更新)
。
判断需不需要扩容:
//判断是否是新增,删除是-1
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//判断是否需要扩容,sizeCtl是扩容阈值
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//n是数组的长度,计算结果的低15位就是n,只不过第16位是1而已
//n最小是16,最大 < MAXIMUM_CAPACITY
//那么rs的范围就是 32795 ~ 32769,大概率是10000000000xxxxx
int rs = resizeStamp(n);
//判断是否有别的线程在扩容
if (sc < 0) {
//极端情况的判断
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//SIZECTL + 1 ,其实就是低16位+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//扩容
transfer(tab, nt);
}
//这一步就是当前没有线程在扩容,CAS去标志
//此时SIZECTL的低16位是2,高16位的最高位是1,剩下15位是数组的长度
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
resizeStamp():
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros(n):
Integer.numberOfLeadingZeros(n)是返回高位的所有的0的个数,具体代码就不分析了,反正就是一顿操作
Java的int类型是32位的
假设n = 2,二进制就是10,那么Integer.numberOfLeadingZeros(n) = 30
n = 20,二进制就是10100,那么Integer.numberOfLeadingZeros(n) = 27
1 << (RESIZE_STAMP_BITS - 1):
RESIZE_STAMP_BITS = 16
1 << (RESIZE_STAMP_BITS - 1) = 1000000000000000,是16位
Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)):
n是数组的长度,最小16,最大MAXIMUM_CAPACITY(1 << 30)
那么Integer.numberOfLeadingZeros(n)的结果就是最小是1,最大是27
( 1 ~ 27 ) | 1000000000000000
计算的结果就是低15位肯定是Integer.numberOfLeadingZeros(n),只不过第16位是1而已
一顿操作,返回值高16位全是0,第16位是1,低15位就是( 1 ~ 27 )之间的值。
第一个复杂点来了,关于已有线程在扩容,协助帮助扩容的逻辑 ,看下这几个极端情况的判断:
//判断是否有别的线程在扩容
if (sc < 0) {
//就是这里的判断
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
//出现上述的情况,就跳出循环
break;
}
再强调一下上面的逻辑是判断是否帮助扩容,而第一个线程进transfer
之后:
sizeCtl:低16位是2,高16位的最高位是1,剩下15位是数组的长度,最高位是1,表示是负数
看下帮助扩容的逻辑:
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
判断1:(sc >>> RESIZE_STAMP_SHIFT) != rs:
rs:低15位就是数组长度,只不过第16位是1而已,大概率是10000000000xxxxx,高16位都是0,正数
RESIZE_STAMP_SHIFT:16
sc < 0,sc = sizeCtl(低16位是2,高16位的最高位是1,剩下15位是数组的长度)
sc >>> 16:就回到了resizeStamp(n)的值,也就是得到高16位
rs = resizeStamp(n),理论上应该是相等的
这里判断不相等,只可能是sc的高16位变化了,而高16代表了数组的长度,意味着扩容成功了
判断2:sc == rs + 1:
sc == rs + 1:因为sc是负数,rs是正数,不可能相等
判断3:sc == rs + MAX_RESIZERS:
MAX_RESIZERS = 65535,sc是负数,rs是正数,MAX_RESIZERS 是正数,sc == rs + MAX_RESIZERS,不可能
关于这2点我想了很久,各种查资料,发现有大佬已经向JDK提出了这个Bug,且被收录了,JDK bug link:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
上面2个判断应该改成:
判断2:sc == rs << 16 + 1:
rs << 16 + 1:左移16位之后,+1,那么低16位就是1,而sc的低16表示的扩容线程数
这里判断相等,就是判断的扩容是否结束扩容,第一个扩容线程的低16是+2,后面每结束一个是-1,最后应该是1
判断3:sc == rs + MAX_RESIZERS:
跟判断2的思路一样,只不过这里是判断扩容线程是否达到上限
判断4:(nt = nextTable) == null:
nextTable只有是扩容的时候才有值, == null也表示扩容结束
判断5:transferIndex <= 0:
transferIndex 只会 == 0,表示最后一个任务已经被领取了,具体实现在扩容方法里面会详解
这几个判断属实牛逼,Doug Lea
牛逼,经常出现一个值表示多种状态的,大佬就是大佬。
transfer():
好了,判断结束了,终于到核心中的核心了,扩容。
同样的,由于较为复杂,先说下大概思路,再show
代码:
- 分配任务
- 领取任务
- 真正扩容
- 扩容结束的善后
分配任务
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//用CPU数量去判断,每个CPU扩容的数组长度,最少16
//为什么是(n >>> 3) / NCPU,而不是直接n / NCPU呢?
//我觉得大概是因为(个人想法):
//1:真正扩容的线程一次不需要领取太多任务,给扩容线程一个机会,让别的线程有机会帮助扩容
//2:尽可能的利用每个CPU,这样put场景多的情况,就会分散压力到每个CPU
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//刚开始扩容,nextTab == null
if (nextTab == null) {
try {
//扩容2倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
//上面的代码会报错,原因只有2个
//1:n << 1结果过大,但是 < Integer.MAX_VALUE,导致OOM,堆空间溢出
//2:n << 1结果超出Integer.MAX_VALUE,导致结果是负数
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
//这里transferIndex是老数组的长度
transferIndex = n;
}
第一部分很简单,就是把数组分成几组,最少16个一组,线程一次扩容一组。
领取任务
int nextn = nextTab.length;
//定义一个ForwardingNode,并把hash变成了MOVED
//当线程发现此下标的hash是MOVED,就知道了,这个下标正在扩容,会帮助扩容
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//标志位,用来标志这个线程需要处理的数组的下标,通过--i,一直遍历
boolean advance = 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;
//判断当前线程领取的任务有没有做完呢,比如说领取了16个,此时的bound为剩余的任务数量
//循环每次--去判断,没完成,就直接false跳出这个循环,继续去干活
if (--i >= bound || finishing)
advance = false;
//transferIndex == 0的时候,表示最后一份任务被领取了
//而走到这个else if说明--i < bound,表示最后一份任务完成了,就会进这个if
else if ((nextIndex = transferIndex) <= 0) {
//将 i 标志位 -1,下面有用
i = -1;
advance = false;
}
//用CAS去标志TRANSFERINDEX,成功即代表该线程领取任务成功
//并把TRANSFERINDEX更新成剩下的区间,最后一份任务是0,这里对应着上面进扩容逻辑判断5
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//把bound改为剩余的任务数量
bound = nextBound;
//i = 剩余任务数量 -1,就是剩下数组的下标最大值
i = nextIndex - 1;
advance = false;
}
}
第二部分看上去挺复杂,接下来也不难。就是遍历数组,然后死循环的去用CAS更新transferIndex
。更新成剩余任务。比如原来数组64,任务每组16个,就把transferIndex更新成48,最后一份就是更新成0。
几个判断也都有详细注释,判断了每组任务做完没有?任务领完没有?
真正扩容
//获取该下标的值,用的getObjectVolatile,强制从主存中获取属性值
else if ((f = tabAt(tab, i)) == null)
//如果为null就用fwd标志当前下标
advance = casTabAt(tab, i, null, fwd);
//到这说明当前下标不是null,并且有线程处理过了
else if ((fh = f.hash) == MOVED)
advance = true;
else {
//锁住下标的头节点
synchronized (f) {
//二次判断一下是否相等
if (tabAt(tab, i) == f) {
//如果不需要改变下标:ln和hn其中一个是null,一个是原来的链表
//如果需要改变下标:ln和hn其中一个是正反一半的链表,一个是反向链表
Node<K,V> ln, hn;
//判断是不是链表
if (fh >= 0) {
//计算值,用来判断是否需要改变下标,跟hashMap一样
int runBit = fh & n;
//lastRun要么是头节点,可以全部挪到新数组
//要么是需要拆分的其中一个链表的尾节点
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是另一个链表的头节点
lastRun = p;
}
}
//为0则代表不需要改变下标
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//同样是遍历,但是多了个p != lastRun:用来判断整个链表是否需要拆分的
//只有走到上面的for里面的if里面,才会不一样
//反之就是一样的,如果一样的,这个for就不会走了
//这里就是为啥遍历2次的原因(减少new Node的次数):
//判断第一个for是否可以直接把链表挪到新数组,就不需要new了
//需要变成2个链表,lastRun后面的同下标的节点不需要new了
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有值,那么这个链表就是一个反向链表,hn就是一个正向链表
ln = new Node<K,V>(ph, pk, pv, ln);
else
//同样
hn = new Node<K,V>(ph, pk, pv, hn);
}
//ln挂到新数组的原下标
setTabAt(nextTab, i, ln);
//hn挂到新数组的原下标 + 老数组长度,跟HashMap一样
setTabAt(nextTab, i + n, hn);
//用fwd占位,进去遍历下一个下标
setTabAt(tab, i, fwd);
advance = true;
}
//红黑树的扩容,跟HashMap一样,其实也是复制的链表,Tree同样维护了一份链表
else if (f instanceof TreeBin) {
//...省略掉
}
}
}
第三部分,其实真的走到扩容这一步,反而很简单的,难的是上面的各种判断。就是很简单的遍历链表(红黑树结构也是遍历的链表)
,然后判断每个节点是否要挂到新的下标,然后CAS挂到新下标,基本上跟跟HashMap一样。
有一个小小的区别,也是一个优化(区别基本上都是优化):
这里遍历了2次,hashmap也就遍历了1次,其实遍历1次就够了,遍历链表,分成2个链表,1个改变下标的链表,1个不改变的。为啥这里遍历2次呢?
第一次for:
//遍历链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
//计算值
int b = p.hash & n;
//判断是否不一致,不一致说明整个链表不能直接挪到新数组,需要拆分挪到2个不一样的下标
if (b != runBit) {
runBit = b;
//此时的lastRun是另一个链表的头节点
lastRun = p;
}
}
第1个for,其实为了lastRun
。
如果此下标的值都不需要改变下标
,那么lastRun = f
,f
是头节点。
那第2个for的判断p != lastRun
就不满足,就不需要走第2个for了。
如果此下标的值有需要改变下标的
,那么lastRun
就是另一个链表的头节点
打个比方:比如此时的数组长度是64
有1,65,193,321,129,257,此时准备扩容,那么走完第1个for,此时的lastRun = 129
,并且后面挂着257,也就是所有在129后面的不需要改变下标的节点。
有1,129,257,65,193,321,此时准备扩容,那么走完第1个for,此时的lastRun = 65
,并且后面挂着193,321,也就是所有在65后面的不需要改变下标的节点。
第2个for:
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有值,那么这个链表就是一个正反一半的链表,hn就是一个反向链表
ln = new Node<K,V>(ph, pk, pv, ln);
else
//同样
hn = new Node<K,V>(ph, pk, pv, hn);
}
注意此时的p != lastRun
满足,就会走这个for。
假设是1,65,193,321,129,257,那么lastRun = 129
走完之后ln是1,129,257,hn是321,193,65
。
假设是1,129,65,193,321,257,那么lastRun = 257
走完之后ln是129,1,257,hn是321,193,65
。
一个是正反一半的链表,一个是反向链表。
仔细看这个for,从头开始遍历,每次指向前面的节点,应该全是反向节点,为啥会出现正反一半呢,因为p != lastRun
。
遍历到lastRun
的时候,因为lastRun
后面跟着原来在他后面的节点(跟lastRun
下标一样的),就不需要遍历了。
这样就少new了一些node
,细想一下,真的超级牛叉,concurrentHashMap
真的精华非常多,很多细小的点,都是极致性能优化。
`扩容结束的善后
//i < 0:只会是-1,表示当前线程是最后一份任务,并且完成了
//i >= n,i + n >= nextn说实话,这2个判断真没看明白,有大佬看到这里,麻烦指点一下
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//判断扩容是否完成
if (finishing) {
nextTable = null;
table = nextTab;
//更新sizeCtl,也就是阈值,1.5倍(用的位运算,最大程度的提高性能)
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)
//默认第一个线程设置sc的低16 +2,一个线程结束了,就会将 sc -1。最后就是1也就是rs +1
//把SIZECTL-1,表示扩容线程-1,注意sc = sizeCtl,是没-1之前的
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//判断是不是最后一个扩容线程
//因为第一个线程扩容是SIZECTL+2,后面再来是SIZECTL+1,每个扩容线程退出时,SIZECTL-1,到最后一个(-1之前),应该是SIZECTL+2
//而(sc - 2)也就是(sizeCtl - 2),sizeCtl的低16位代表了扩容线程,如果最后一个那就是2,-2就是0
//resizeStamp(n) << RESIZE_STAMP_SHIFT的低16位全是0,要是不相等说明不是最后一个
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
//到这说明是最后一个扩容线程,把finishing改为true,把i改成n
//i改成n 是为了再检查一遍,是否全部扩容完毕,--i >= bound每次会满足,然后再走下面的else if去一一校验
//finishing改为true 是为了上面的一步全部检查之后,就可以真正退出了
i = n; // recheck before commit
}
}
这里其实还是扩容之前的判断,记住扩容的时候sizeCtl
的高16
表示数组的长度,最高位是1
,低16
表示扩容的线程数+1,因为第一个线程扩容是+2,后面是每多一个线程就是+1。
想通了这个,这些判断就很好理解了,Doug Lea
牛逼。
长舒一口气,扩容终于结束了。
helpTransfer():
好了,大家冷静一下,回到put
的第二部分的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;
}
哈哈,没有一句注释。其实这里跟准备扩容的判断一模一样,大家可以自己加注释,复习一遍。
好了,整个put
全结束了,精华太多了,本次就分享到这,还有一些细节,大家可以去看我的下篇文章:细节篇
,下篇分析ThreadPoolExecutor
的,或者大家也可以留言分析啥。