目录
HashMap、HashTable和ConcurrentHashMap在现在的日常面试中基本已经是必问问题,主要是从基本使用,数据结构,源码分析等多个方面被问及到。但是很多人在面试中很难说清楚面试官问及到的问题。本文对ConcurrentHashMap进行一个大概的解读。通过理解其中的思想,来体会ConcurrentHashMap的神奇之处。
一、为什么要使用ConcurrentHashMap
面试中,基本第一个问题就是会被问及到,为什么我们不用HashMap或者Hashtable而使用ConcurrentHashMap呢?
HashMap线程不安全,在并发场景下可能会出现线程安全问题。相信这大家都会不假思索的说出来。那面试官又会问,那Hashtable是线程安全问题,为什么不用呢,因为本文主要分析ConcurrentHashMap,所以简单介绍一下HashMap和Hashtable。
HashMap
JDK1.7(闭环)
在JDK1.7之前,HashMap有个重大的线程安全问题就是 "闭环" 问题,相信很多小伙伴也知道,简单来说就是扩容时,因为多个线程操作扩容,可能会使得扩容形成闭环。
举个例子,现在有个Map上某个元素位置有个链表,上面有a,b两个元素。
然后数据需要进行数组扩容,我们看下数组扩容的源码,了解JDK1.7扩容的最大问题,'头插法'
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
//e为空时循环结束
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 成环的代码主要是在这三行代码
// 首先插入是从头开始插入的
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
首先线程A先进来,拿到数据进行扩容,步骤一,newTable[i] = null,e = a,所以e.next = null,所以newTable[i] = a,next = e = b,这时候时间片切换到线程b,然后线程B执行操作。
执行相同的步骤一,然后继续步骤二,e = b, 此时e.next = null,e.next = newTable[i] = a, newTable[i] = b, e= null,执行结束。这时候在线程B的执行完成后。Map结构变成:
这时候,有两个注意的点,
- 原table的数据是线程共享,线程B在修改扩容后的新table结构的同时,也会给原有table的引用修改,也就是说现在原有table也会变成 b.next = a。
- newTable是线程独有的,看上层源码可以知道,每次都是new出来的新Table。
这时候线程B执行完毕,CPU时间片切换到线程A,这时候线程A继续步骤一继续执行,这时候步骤二就有个微妙的变化,因为线程B执行完成修改原有数据引用,并且在执行完成后线程A也读取到变化,所以,这时候线程A的步骤二就会变为,e=b,e.next = a,e.next = newTable[i] = a,newTable[i] = b,e = a ,然后执行步骤三,e.next = null(线程一执行结果),这时候你就会发现按照上面步骤执行,a.next = newTable[i] = b, newTable[i] = a,e = null。但是线程B已经完成 a.next = null,线程A的执行完成 a.next = b,覆盖线程B的执行结果。这时候就会出现我们所忌讳的 "闭环" 现象。
再当我们执行get()方法时,因为Map是在链表逐条查找,所以这种闭环就会引起死循环的发生。
JDK(1.8)
在JDK1.8及以后,官方为了解决这个闭环的问题,扩容由原来的头插法改为使用尾插法来解决,但是这个问题解决了还是存在两个线程安全的问题;
- 多个线程同时添加数据的时,如果发生碰撞,会造成数据覆盖,而造成添加数据丢失。
- 多线程并发扩容,如果扩容后发生数据碰撞,也会覆盖数据,造成丢失。
所以,HashMap数据不安全这个短板是毋庸置疑的了。
当然,如果我们既想使用HashMap,又想让其实线程安全的集合,官方给我们提供了一个技巧。
Collections.synchronizedMap(new HashMap<>());
Hashtable
Hashtable是线程安全的集合,但是在高并发的场景下我们为什么还是不建议使用呢?我们不妨看一下Hashtable的源码是如何解决线程安全问题的。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
简单粗暴,在方法上直接使用 synchronized 关键字,这样就保证多线程并发条件下,保证单个集合的数据安全。这样直接在方法上上锁,看似没什么问题,但是效率这一块我们就需要考虑了。
synchronized 在JDK 1.6 之前其实就是重量级锁,也就是直接阻塞线程,在JDK1.8之后采用锁升级,无锁、偏向锁、轻量级锁、重量级锁。也就是说在并发度高的情况下还是直接阻塞线程。
直接方法上锁,这样如果某一线程争抢到锁,其余的线程将全部阻塞在方法外,很影响存储效率。
二、ConcurrentHashMap源码解析
concurrentHashMap的使用方法和HashMap基本没什么差别,基本都是put(key,value),get()两个常用的方法。但是concurrentHashMap是如何保证高效而且多线程并发情况下的线程安全的呢?
ConcurrentHashMap的数据结构
concurrentHashMap的数据结构: 数组+链表+红黑树
草率的画了个图,大致是这个样子。map的开始是数组加链表,但是当前链路大于8,数据长度大于64,再往当前链表添加数据就会将当前链表转化为红黑树,
如果在扩容的时候 当前链表长度小于8 了 又会将链表从红黑树 转化为链表。
红黑树是一个特殊的平衡二叉树,当数据结构切换成红黑树的时候,又可以提高数据的查询效率。本文暂时不对红黑树做过多的介绍。
ConcurrentHashMap是如何保证线程安全的呢?
concurrentHashMap通过使用synchronized 和 cas(乐观锁) 共同实现线程安全操作。与Hashtable对比来看更灵活而且效率更高。
- hashtable是对整个put()方法进行上锁。而concurrentHashMap是对当个元素盒子上锁,后面的源码分析会有体现。
- hashtable扩容只有一个线程完成且多余线程均会阻塞,而concurrentHashMap支持多线程并发扩容而且巧妙的做到了线程隔离。
以上两点就完全体现出类concurrentHashMap的强大优势。
ConucurrentHashMap分析
put()
public V put(K key, V value) {
return putVal(key, value, false);
}
putVal()
假设当前线程ThreadA初始put(),集合进行初始化操作。
首先我们不需要通篇全部阅读代码,我们所需要做的是根据场景进行分析,这样可以更清楚的阅读源码。
当集合首次被填充数据是,table==null,并且集合length == 0,所以table需要初始化initTable(),这里有一个编程小技巧。
注意: Node<K,V>[] tab = table,细心的人可以发现,table已经是定义好的变量,为什么还要设置一个局部变量然后进行传递呢,这不是代码冗余么?其实这样在问题跟踪的时候很方便,大家在开发的时候可以自己尝试一下这个小技巧。
重要参数:
transient volatile Node<K,V>[] table; // Node 节点
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 获取当前 key 的Hash值
int binCount = 0; // 链表长度
for (Node<K,V>[] tab = table;;) // 自旋进行集合填充 {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 1、 当前table为Null 或者 集合长度为 0
// 2、 初始化 table
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) // 判断当前位置是否有数据占位{
// 如果当前位置没有数据 进行数据填充 结束当前初始化
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 当前集合初始化完成
// 为了防止并发操作 ,在操作当前数组Node的时候
// 进行 sync 锁定 ,注意只锁住当前元素的链表位置,别的是不会锁住的
synchronized (f) {
// 拿到当前最新的元素
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 进行链表拼接扩容
binCount = 1; // 链表长度,默认是1
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 判断当前是否是同一key元素,如果是 覆盖
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 如果不是同一key 链表拼接
// next 指向链表的下一元素位
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 1 每次只增加一个元素
// binCount 链表长度
addCount(1L, binCount);
return null;
}
initTable()
这个方法里体现出了一个变量,这个变量是concurrentHashMap设计的一个非常巧妙地地方,我先标记出来,后面的分析后有所体现。
sizeCtl
- -1 // 标识当前集合正有线程进行初始化
- >0 // 扩容长度
- <-1 // 代表有几个线程正在扩容 -2 代表有一个线程正在扩容
初始化的逻辑分析:
- 判断当前table是真的未初始化,==null,length == 0。
- 判断标记位 sizeCtl 是否为 -1,如果是的话证明当前已经有线程进行初始化,所以当前线程让出资源。
- 如果当前么有线程初始化,compareAndSwapInt()方法cas原子操作,修改标识未为-1,证明已经有线程进行初始化。
- 初始化线程进行集合初始化,其实很简单就是初始化一个默认长度为16的Node[]数组。
- sc = n - (n >>> 2);计算扩容长度
- sizeCtl 赋值扩容长度
经过集合初始化,集合初始化完毕,sizeCtl的标识两种含义也得到了体现。
private transient volatile int sizeCtl; // 记录标识位
private static final int DEFAULT_CAPACITY = 16; // 默认长度 16
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// 证明已经有线程进行初始化 让出
Thread.yield(); // lost initialization race; just spin
// CAS 操作 修改当前标示位 证明已经有线程进行修改
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 拿到默认长度 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 初始 table
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 赋值
table = tab = nt;
// 实质就是 n * 0.75(扩容因子)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc; // 将扩容长度重新赋值
}
break;// 停止
}
}
return tab;
}
tabAt()
这里需要注意两个问题。
1. 为什么不直接 table[i] 去进行判断呢?
因为当前操作是并发场景,有可能当前table已经被修改,但是拿到的并不是最新的值。
2. volatile修饰保证可见性,为什么还不是最新的呢?
因为volatile只能在修饰 table 数据情况下,只能保证当前引用可见,并不能保证当前数组内的数据是完成最新的数据,所以当别的线程进行数据的修改了,当前线程还是无法拿到最新的数据,所以才用tabAt方法保证原子性,可见性。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// 原子操作 判断当前位置是否已经有元素
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
addCount()
- 集合长度修改
- 判断当前集合是否需要扩容
修改集合长度,这里有两个点需要进行思考
1. 为什么不用cas来解决长度并发修改问题?
2. 为什么不用锁来解决并发修改的问题?
CounterCell()
数组 -- > 解决并发线程 -- > 修改元素个数(元素value总和)
CounterCell[] 数组实现多个线程同时操作集合, 进行集合元素个数的修改
这时候大家可能还不是很理解什么叫做数组完成当前集合长度的修改。
我们思考一下HashMap的元素个数是通过修改size来得到当前集合的元素个数,但是会有并发问题,而concurrentHashMap不仅想解决并发问题,还想在不使用cas和lock的前提下,
不仅解决线程安全问题,而且提高线程利用率,来记录元素个数,这时候就体现出了这个数组的神奇之处。
假设当前有两个线程A,B,同时往集合中添加数据。
- 这时候concurrentHashMap就会初始化一个CounterCell[]数组,每个数组中大致可以理解为(key,value)这样的结构,初始长度为2。
- 当线程A添加完成数据并需要修改元素个数时,通过 rs[h & 1],这个大家可以理解为一个路由算法,也就是负载均衡机制,判断当前线程落点在CounterCell[]的哪个位置。
- 根据路由算法拿到落点,假设落点是CounterCell[1],这时候就修改当前数组的value,value + 1,因为添加元素是一个一个添加,所以value++累加。
- 同样线程B也根据同样的逻辑进入落点进行value修改
- 最后计算当前集合的元素个数时,只需要统计CounterCell[]数组的元素value的值累加和即可。
private static final long BASECOUNT; // 集合元素个数的基础属性 默认等于 0
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 元素个数增加
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) // 默认尝试一次 如果没有并发 使用 baseCount 来记录元素个数{
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// 判断当前随机数 在 CounterCell[] 已经存在
// 就在其基础上直接累加扩容
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 扩容判断
// check 当前链表长度大于1 就可以进行下一步操作
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 1. s 当前集合元素个数 大于等于 扩容元素个数 sizeCtl
// 2. table 集合不是 NULL
// 3. 集合长度小于 最大集合数
// 以上条件均满足 就可以进行扩容操作
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 1. 记录扩容戳唯一 也就是记录当前扩容周期唯一
// 2. 记录当前扩容的线程数
else if (U.compareAndSwapInt(this, SIZECTL, sc,
// 位移 16 位 + 2
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount()
这个方法其实就是在做CounterCell[]的初始化,和线程落点,value修改的相关操作,来保证元素个数的正确性。
// cellsBusy 标识位
// 1. 证明当前正有线程进行元素个数修改
// 如果当前 cellsBusy 并且 counterCells == as == null
else if (cellsBusy == 0 && counterCells == as &&
// 首次进入 cellsBusy 修改为 1 证明有线程初始化
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
// 初始化
CounterCell[] rs = new CounterCell[2];// 初始长度为2的数据
// 1. rs[h & 1] 路由算法 负载数据 判断当前线程落在数组的哪个元素上
// 2. x = 1, 落入对应的数据上进行value +1 也就是元素个数 +1
rs[h & 1] = new CounterCell(x);
// 初始化完成进行 CounterCell[] 赋值
counterCells = rs;
// init为 true 初始化完成
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 第二次进来 已经初始化完成
// 判断当前数据位置是否为 NULL 进行其余线程的元素个数修改
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy == 0 不存在线程修改
// CounterCell r = new CounterCell(x) // 赋值操作
if (cellsBusy == 0) { // Try to attach new Cell
// value 值修改增加
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
// 修改标识位
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
// 赋值操作
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
sumCount()
元素个数统计
// 1、遍历当前的 CounterCell 数据
// 2、value 进行累加 拿取元素个数
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
方法总结
- put()调用putVal()进行数据添加
- putVal()首先集合初始化,然后判断元素位是否有元素,无直接添加,有的话判断是否同一元素,是,覆盖,否,进行链表拼接
- 添加的时候使用 sync(f),锁住当前元素槽,不影响其他槽位的元素添加
- initTable()初始化
- addCount()进行元素个数统计和扩容判断
concurrentHashMap扩容
回顾addCount()方法中的扩容逻辑
这时候 sizeCtl的第三个标识含义就起了作用,上文有提及到。
当 sizeCtl < -1 时,证明当前有线程在扩容。
但是看代码可以发现,如果有线程在扩容,另外的线程还是可以继续帮助扩容的,只需要在扩容的标识位进行累加记录扩容线程数即可。
这就是concurrentHashMap的又一特性,多个线程并发扩容。那么他是如何保证多个线程并发扩容的呢?
- 1.resizeStamp(),扩容戳,记录扩容的数组长度及线程数目
- 2.分段扩容,根据当前CPU,进行线程分段,每个线程只负责自己段内的扩容
- 3.高低链分类,完成快速扩容
- 4.扩容的元素盒子会被修改为 fwd(MOVED)状态,这时候别的线程是不会再操作当前元素盒子的
比如当前A,B两个线程对一个32位的数组执行扩容操作,这时候根据算法进行扩容区间拆分,A分到0-15,B分到16-31。
这时候A,B两个线程已经有了自己负责的区间,双方是互不打扰的。
// 扩容判断
// check 当前链表长度大于1 就可以进行下一步操作
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 1. s 当前集合元素个数 大于等于 扩容元素个数 sizeCtl
// 2. table 集合不是 NULL
// 3. 集合长度小于 最大集合数
// 以上条件均满足 就可以进行扩容操作
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 1. 记录扩容戳唯一 也就是记录当前扩容周期唯一
// 2. 记录当前扩容的线程数
else if (U.compareAndSwapInt(this, SIZECTL, sc,
// 位移 16 位 + 2
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
resizeStamp()
该方法涉及到位运算,大致可以理解为计算扩容戳,记录扩容数组信息和扩容线程数
1. RESIZE_STAMP_BITS 16
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
transfer()
这里会比较主要的就是线程分工和高低链。线程分工上文已经说了,这里主要说一下高低链。
HashMap扩容判断是重新计算每个元素的Hash值,然后重新每个放到新的槽位上,但是这样很耗时,并且很笨重,那么可不可以只计算一次Hash值然后就可以一起进行数组扩容呢?
concurrentHashMap使用高低链,通过算法计算,将原来的链表进行重组,放到高位链和低位链中,当计算完毕,低位链位置不变,高位链放入新的位置,这样就可以一次性的完成一个槽位的扩容。
// fh = f.hash fh 就是当前槽位的 hash 63 行
// A :fh & n = 0
// B :fh & n != 0
假设上述算法 = 0的放入低位链,不等于0的放入高位链
// NCPU 当前CPU MIN_TRANSFER_STRIDE 16 处理最小数组长度
// n >>> 3 当前数组长度除以 8
// < 16 stride = 16 意思就是保证每个线程可以处理 16 个长度的数组扩容
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 初始扩容数组 n << 1 左移一位
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;
// fwd 标识位 如果有线程处理当前数据 标识
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;
}
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
}
}
// 如果当前槽位 为 NULL 设置为 fwd
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 如果当前为 MOVED 也就是 fwd 证明迁移已将完成
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) {
// 通过计算 得到结果 对链表进行分类
// fh = f.hash fh 就是当前槽位的 hash 63 行
// A :fh & n = 0
// B :fh & n != 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;
}
}
// 最终分类
// ln :低位链
// hn :高位链
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);
}
// CAS 操作
// 低位链 位置不变
setTabAt(nextTab, i, ln);
// 高位链 位置迁移
setTabAt(nextTab, i + n, hn);
// 修改标识 fwd 标识
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;
}
}
}
}
}
}