transfer方法是是在addCount 方法中被调用的。
- 情况一:当该线程发现其它线程在扩容时,调用transfer 协助扩容
- 情况二:当前线程触发扩容条件,调用transfer进行扩容
扩容的新数组是旧数组的两倍,旧数组的fwd节点 引用连接着新数组
当迁移该桶位时,会用synchronize 锁住桶位的头节点
ConcurrentHashMap的数组中有三种节点:
- 普通节点
- TreeBin节点
- FWD节点
- 普通节点:普通链表节点,单向链表
- TreeBin节点:里面维护两种形式的节点,一种是TreeNode树节点(红黑树),一种是TreeNode链表节点(双向链表)
- FWD节点:是链接新的数组的引用
在数组扩容后,进行迁移链表节点时就是按上面这个图进行的。
注:Treebin 头节点下面有红黑树 也有 链表
transfer源码
* 当触发扩容时调用 transfer时,nextTbable 为空
* 当不触发扩容时,协助扩容时,调用 transfer,nextTbable 不为空
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// n 表示扩容之前table的数组长度
// stride 分配给线程任务的步长 比如128长度数组 要迁移到 256 长度的数组中。需要步长
int n = tab.length, stride;
// 方便讲解 stride 固定为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 条件成立:nextTab==null 为true,表示触发扩容
// 条件不成立:nextTab!=null,表示协助扩容
if (nextTab == null) { // initiating
try {
// 创建了一个比table 大一倍的数组
@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;
// 记录迁移数据整体位置的一个标记,index 计数从 1 开始(从16 到 1)
transferIndex = n;
}
// 表示新数组的长度
int nextn = nextTab.length;
//fwd节点,当前桶位数据处理完毕后,将此桶位设置为fwd节点,其它写线程 或读线程看到后,会有不同逻辑
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 推进标记
boolean advance = true;
// 完成标记
boolean finishing = false; // to ensure sweep before committing nextTab
// 自旋
// i 表示当前线程任务 执行到的桶位
// bound 表示给当前线程任务的下界限制 比如说:迁移fwd是从高位到地位,15 -> 10 bound表示这个范围,低于10 就越界了
for (int i = 0, bound = 0;;) {
// f 桶位的头节点
// fh 头节点的hash
Node<K,V> f; int fh;
/**
* 1.给当前线程分配线程区间
* 2.维护当前线程任务进度(i 表示当前处理的桶位)
* 3.维护map 对象全局范围内的进度 (advance = true 代表继续推进)
*/
while (advance) {
// 分配任务区间有关系
//nextIndex 分配任务开始下标
//nextBound 分配任务结束下标
int nextIndex, nextBound;
//CASE1:
// --i >= bound 成立表示:如果当前线程的任务还没有完成
// 还有相应的区间的桶位要处理
// 不成立:表示当前线程任务已经完成 或 未分配
if (--i >= bound || finishing)
advance = false;
//CASE2:
// 前置条件:当前线程任务已完成 或 未分配
// 条件成立:表示全局范围的桶位都分配完毕了,没有区间可分配了,设置当前线程的i变量为 -1 跳出循环后,执行退出迁移任务相关的程序
// 条件不成立:表示对象全局范围内的桶位尚未分配完毕,还有区间可分配
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//CASE3:
// 前置条件:1.当前线程需要分配任务区间 2.全局范围内还有桶位尚未迁移
// 条件成立:说明给当前线程分配任务成功
// 条件失败:说明分配给当前线程失败,应该是和其它线程发生了竞争吧
// TRANSFERINDEX 地址上的值如果是nextIndex,就改变为(nextIndex - stride == 0)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//任务结束的下标
bound = nextBound;
// transferIndex 是从 1开始的,nextIndex是从 0 开始的,所以需要 -1
i = nextIndex - 1;
advance = false;
}
}
/** 处理线程任务完成后,线程退出transfer 方法的逻辑 */
// 条件一:i < 0
// 成立:表示当前线程未分配到任务
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 保存sizeCtl 的变量
if (finishing) {
nextTable = null;
table = nextTab;
// sizeCtl = 2n - 1/2n = 3/2n = 0.75 * 2n (下次扩容的阈值)
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// sizeCtl 后16位是线程数量,退出前需要将数量 -1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//1000 0000 0001 1011 0000 0000 0000 0000
// (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT:说明当前线程是最后一个退出transfer的线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 不是最后一个线程,直接退出
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
/**
* 线程处理一个桶位数据的迁移工作,处理完毕后设置advance 为true 表示继续推进,然后就会回到for
*/
// case2到case4的 前置条件:当前任务尚未处理完,正在进行中
//CASE2:
// 如果当前桶位的头节点是null,只需要让它指向fwd节点
else if ((f = tabAt(tab, i)) == null)
// 没有数据 不用加锁
advance = casTabAt(tab, i, null, fwd);
//CASE3:
// 条件成立:说明当前桶位已经迁移过了,当前线程不用再成立了,直接再次更新当前线程任务索引,再处理下一个桶位 或者其他操作
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//CASE4:
// 前置条件:当前桶位有数据,而且node节点 不是fwd节点, 说明这些节点需要迁移
else {
// 加锁 当前桶位的头节点
synchronized (f) {
// 再次判断锁的头节点对不对,上面的条件与该else之间,f可能被更改,锁的不对需要自旋
if (tabAt(tab, i) == f) {
// ln表示低位链表的引用
// hn表示高位链表的引用
Node<K,V> ln, hn;
// 条件成立:当前桶位是链表桶位
if (fh >= 0) {
// lastRun
// 可以获取出 当前链表 末尾连续
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;
}
}
// 说明链表后面连续的是 0xxxx ,b是0,ln低位链表指向lastRun,后面不用再创建了
if (runBit == 0) {
ln = lastRun;
hn = null;
}
// 说明链表后面连续的是 1xxxx ,b是1,hn高位链表指向lastRun,后面不用再创建了
else {
hn = lastRun;
ln = null;
}
//迭代链表:跳出条件:当前循环节点 不等于lastRun
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)
// 创建node(ph,pk,pv) 插入 到低位链表的头部
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);
// 把原来表的 i桶位 设置为fwd节点。fwd指向新表
setTabAt(tab, i, fwd);
advance = true;
}
// 条件成立:当前桶位是红黑树代理节点 TreeBin
else if (f instanceof TreeBin) {
// 转换头节点为 treeBin引用 t
TreeBin<K,V> t = (TreeBin<K,V>)f;
// 低位双向链表 lo 指向 低位链表的头,loTail指向 低位链表的尾巴
TreeNode<K,V> lo = null, loTail = null;
// 高位双向链表 hi 指向 高位链表的头,hiTail指向 高位链表的尾巴
TreeNode<K,V> hi = null, hiTail = null;
// lc 表示低位链表元素数量
// hc 表示高位链表元素数量
int lc = 0, hc = 0;
// 迭代TreeBin 中的双向链表,从头节点到尾节点
for (Node<K,V> e = t.first; e != null; e = e.next) {
//h 代表循环当前节点的hash值
int h = e.hash;
// 使用当前节点 构建出来的新的TreeNode
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;
// 数量+1
++lc;
}
// 当前节点 应该加入到高位链中
// 高位和低位一样
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 低位链表数量小于阈值6,把低位链双向链表TreeNode 转换为单向链表node
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
// hc != 0 说明高位也有数据,有两个链表,需要再创建TreeBin放低位链表
// 如果高位没有数据,也就是说没有分成两条链表,只有一条,就直接把原来的 t放进去就行了,不用再创建了
(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);
// 原来的链表放fwd节点,指向新表
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
helpTransfer源码
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// nextTab 引用的是 fwd.nextTable == map.nextTable 理论上是这样
//sc 保存map.sizeCtl
Node<K,V>[] nextTab; int sc;
// 条件一:tab != null 恒成立
// 条件二:f instanceof ForwardingNode 恒成立
// 条件三:nextTab = ((ForwardingNode<K,V>)f).nextTable) != null 恒成立
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 拿到当前表的 扩容戳 假设 16 -> 32 扩容戳:1000 0000 0001 1011
int rs = resizeStamp(tab.length);
// 条件一:nextTab == nextTable
// 成立:表示当前扩容正在进行中
// 不成立: 1.nextTbale 被设置为null,扩容完毕后,会被设为null
// 2.再次触发扩容。。拿到的nextTab已经过期
// 条件二:table == tab
// 成立:什么扩容正在进行中,还未完成
// 不成立:什么扩容已经结束了,扩容结束之后,最后退出的线程会设置 nextTable 为 table
// 条件三:(sc = sizeCtl) < 0
// 成立:说明扩容正在进行中
// 不成立:说明sizeCtl当前是一个大于0的数,此时代表下次扩容的阈值,当前扩容已经结束
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs
// true-> 说明当前线程获取到的扩容唯一标识戳 非 本次扩容
// false-> 说明当前线程获取到的扩容唯一标识戳 是 本次扩容
// 条件二:jdk1.8中有bug_jira:其实想表达的是:sc == (rs << 16) + 1
// true-> 表示扩容完毕,当前线程不需要再参与进来了
// false-> 扩容还在进行时,当前线程可以参与进来
// 条件三:jdk1.8中有bug_jira:应该是:sc == rs << 16 + MAX_RESIZERS
// true-> 表示当前参与并发扩容的线程达到最大值 65535 - 1
// false-> 表示当前线程可以参与进来
// 条件四:transferIndex <= 0
// true-> 说明map 全局范围内的任务已经分配完了,当前线程进去也没活干了
// false-> 还有任务可以分配
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;
}