没看过前面的推荐先看前面,
ConcurrentHashMap源码分析,轻取面试Offer(一)
ConcurrentHashMap源码分析,轻取面试Offer(二)
上一篇我们引出了addCount这个函数,简单分析了它做的事情,但还有很多事情我们不知道呢,下面继续讲解
先来看一下注释过的函数
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add
* @param check if <0, don't check resize, if <= 1 only check if uncontended
翻译:
*添加计数
如果表太小且尚未调整大小,则调用transfer。
如果正在调整大小,且还有活干,那就去帮忙去。
在转移之后重新检查占用率,看看是否已经需要再调整大小,因为调整大小是滞后的添加。
*@ PARAM-X要添加的计数
* @ PARAM检查是否为0,不检查调整大小,如果<=1只检查是否未争用
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//..省略处理高并发的部分
if (check >= 0) {//put可以进来
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
······#### 从这儿开始 ####·······
//sc=sizeCtl初始是阀值,sc肯定大于0,先不看这
if (sc < 0) {
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);
}
//启动transfer,
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*
翻译:
返回用于调整大小为n的表大小的戳位。当由RESIZE_STAMP_SHIFT向左移位时,必须为负。
(这是预先处理)
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
//Integer.numberOfLeadingZeros就是返回第一个1前面有几个0,
//(1 << (RESIZE_STAMP_BITS - 1)) :让1左移 (16 - 1)位
}
/*
说明,这里返回值不一定负数,但是这个返回值进行 << RESIZE_STAMP_BITS 时候就一定是负数了
因为返回值中第15位一定是1,
int一共32位
再向左移16位,就是说符号位一定是1,即负数
*/
看上面函数的下部分
if (check >= 0) {//删除时不会进来,也就是我们put方法会进来
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
······#### 从这儿开始 ####·······
//sc=sizeCtl初始是阀值,sc肯定大于0,先不看这
if (sc < 0) {
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);//这里是帮助扩容时候复制
//其中,这个 + 1 拿小本本记一下
}
//启动transfer,
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//这里进行左移16位,进来的时候sc的第15位是1,cas运算完了,如果成功,那么sc一定是负数,以后肯定会走上面那个 if (sc < 0),去帮助进行transfer
//其中,这个 + 2 可以拿小本本记一下
transfer(tab, null);
s = sumCount();
}
}
到这里,我们知道了put完毕大概要做的事情,接下来看扩容。
transfer这个方法就牛逼了。
为了简单,先看思路:
切入点:调用、退出,条件语句,
先从上往下看,不好看懂了,找下一个切入点 if 判断的是啥变量,这个变量干啥的,啥时候值变了,或者找啥时候return的
/**
* Moves and/or copies the nodes in each bin to new table.
翻译:
移动或者复制节点到新table.
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;//n表示表长,stride为步幅,也就是扩容时,每个扩容任务的跨度
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//NCPU 是cpu的核心数,单核cpu就只由一个线程做,多核:表长/8/cpu核心数,最少为16
if (nextTab == null) { // 这里和initTable代码类似,就是初始化新表,类比下就好
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);
/* ↑
看 ForwardingNode ,发现这个类的hash值就是MOVED。
第一篇博客讲到:在put里面曾有个判断
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
也就是ForwardingNode 这个类代表着该结点全都被搬走了,而put时候如果发现 == MOVED ,就执行helpTransfer(tab, f);也就是说看到搬走了,先不put了,先去帮忙搬家去。
*/
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) {// 当advance == true时,表明该节点已经处理过了
int nextIndex, nextBound;
if (--i >= bound || finishing)// --i ,遍历原hash表中的节点
//(正常情况下,如果大于 bound 不成立,说明该线程上次领取的任务已经完成了。那么,需要在下面继续领取任务
advance = false;
// 这里的目的是:1. 当一个线程进入时,会选取最新的转移下标。2. 当一个线程处理完自己的区间时,如果还有剩余区间的没有别的线程处理。再次获取区间。
else if ((nextIndex = transferIndex) <= 0) {
// 如果小于等于0,说明没有区间了 ,i 改成 -1,推进状态变成 false,不再推进,表示,扩容结束了,当前线程可以退出了
// 这个 -1 会在下面的 if 块里判断,从而进入完成状态判断
i = -1;
advance = false;
}
//还没忙完呢,别闲着,快来干活,领份工作给你
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;// 这个值就是当前线程可以处理的最小当前区间最小下标
i = nextIndex - 1;// 初次对i 赋值,这个就是当前线程可以处理的当前区间的最大下标
advance = false;
// 这里设置 false,是为了防止在没有成功处理一个桶的情况下却进行了推进,这样对导致漏掉某个桶。下面的 if (tabAt(tab, i) == f) 判断会出现这样的情况。
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {//都搬完了
nextTable = null;
table = nextTab;// table 指向nextTable
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//cas方式-1,这里 - 1跟小本本上之前记的 +1 对应起来了,也就后16位代表参加搬家工作的线程的个数,
//到这说明这个线程干完一份工作了,在回到上面看看还有没有活干,有的话干活,没活干等其他人干完搬家结束。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
//cas方式-1,这里 - 1跟小本本上之前记的 +2 对应起来了,一旦+2代表搬家完全结束
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)//如果为null,则把fwd方上,相当于标记了MOVED,以后有线程put时想到这,直接喊他来帮忙
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//这里Doug Lea 注释对线程说:这里已经加工过了,你可以重新另一份工作了
else {
synchronized (f) {//····这里给当前节点加锁!
if (tabAt(tab, i) == f) {//DCL双重锁
Node<K,V> ln, hn;
if (fh >= 0) {
//搬运链表..略
//当前核心是讲put、不过自己看的时候要注意hash是由规律的
//每隔着两个,下两个的新坐标 + n 或为原来的
// advance = true 可以执行--i,遍历节点
}
else if (f instanceof TreeBin) {
//搬运红黑树...略
// 扩容后树节点个数若<=6,将树转链表
// advance = true 可以执行--i,遍历节点
}
}
}
}
}
}
扩容先探讨到这儿,发现扩容我们也掌握了,回过头想一下put的时候正在扩容会干嘛。
1.正常,锁住该节点,把自己的key 放上去,解锁走人,美滋滋。
2.正在被搬家,别人已经挂了锁,则线程阻塞,直到这个节点搬完,这时候就被DCL认出来了,重新while循环到检测是否是pwd那,参考3
3.要放的节点为fwd,此时该线程就回去帮忙扩容,等一起扩容完了然后再回到put,干自己的事情。
从开始看到现在只是笔者带着大家过了一遍put,其他API、其他源码、其他新框架看起来的时候也是这么看,相信多看两遍遍这三篇简单的博客,很快就能入门看源码,可以继续看ConcurrentHashMap 其他 api ,慢慢扩展。
若无催更,暂不继续更新。
若有疑问或我写的有问题,欢迎点评和指正。