首先,并发场景下, 需要考虑线程调度时间片切换的各种问题, 所以并发场景下的各种问题都挺复杂的, 我今天主要为了解决 hashmap 1.7时, 并发插入, 导致死循环的过程, 图文并茂哦, 希望点赞支持 , 该问题, 也是面试有概率问的!!!
源码
首先我先将扩容操作的代码摆放这里
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
// 遍历老的数组元素,移动到新数组里
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
// 重点看这里的几行代码,采用的是头插法
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
产生死循环过程
hashmap 在长度为 2 的时候, 由于loadFactor = 0.75, 所以扩容阈值是 0.75 * 2 = 1.5, 当我们再往内部插入元素时, 会导致调用 resize() 方法进行扩容, 最终调用到 transfer 方法中, 进行元素的搬运.
假设最初的 hashMap 如下:
此时两个线程同时想要put一个值进入, 分别为线程A (put 5), 线程B (put 7), 假设线程A 先获取到执行机会, 插入5, 注意代码中使用的是头插法, 所以 5 在 3 之前.
然后CPU 切换时间片, 由线程B 执行, 插入 7节点,
之后线程 A 和 B 都意识到需要扩容操作, 所以都会调用 transfer 方法
我们假设, 线程 B 此时仍然在执行, 并且执行到红色代码处 又被 CPU 切换到 A 线程执行, **此时 B 这边的 e = 7, next = 5 (注意很重要! 很重要! 很重要!) **
由于采用的是头插法, 所以说这会导致链表被逆序回来, 形成这个样: (这里假设他们重新计算hash值仍然在该位置上~)
这是 线程 A 成功执行完成, 轮到 B 线程进行执行, 此时的指针情况是这样的
线程 B 先创建一个新的数组, 然后 先将 e 挪动过去, 然后 e 指向 next , next 执行 e.next, 也就是这样: (注意e的切换和next的切换 要看上图的右边 ! 注意e的切换和next的切换 要看上图的右边 ! 注意e的切换和next的切换 要看上图的右边 !)
然后到了出现死循环的地方, 就是此时由于头插法, 执行 e.next = entry[1] —> 此时的 e = 7, next = null. 执行后, 就是