HashMap在高并发下引起的死循环

探讨了HashMap在多线程环境下可能导致死循环的问题,分析了其内部机制,特别是在resize操作时可能产生的链表回路,以及如何使用ConcurrentHashMap避免此类问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap其实并不是线程安全的,在高并发的情况下,是很可能发生死循环的,由此造成CPU 100%,这是很可怕的,所以在多线程的情况下,用HashMap是很不妥当的行为,应采用线程安全类ConcurrentHashMap进行代替。

 

 

HashMap死循环原因

HashMap进行存储时,如果size超过当前最大容量*负载因子时候会发生resize,首先看一下resize原代码

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }


而这段代码中又调用了transfer()方法,而这个方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生get()死循环,我们看一下他的源代码

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死循环演示

假如有两个线程P1、P2,以及链表 a=》b=》null

1、P1先执行,执行完"Entry<K,V> next = e.next;"代码后发生阻塞,或者其他情况不再执行下去,此时e=a,next=b

2、而P2已经执行完整段代码,于是当前的新链表newTable[i]为b=》a=》null

3、P1又继续执行"Entry<K,V> next = e.next;"之后的代码,则执行完"e=next;"后,newTable[i]为a《=》b,则造成回路,while(e!=null)一直死循环

 

总结

HashMap并非线程安全,所以在多线程情况下,应该首先考虑用ConcurrentHashMap,避免悲剧的发生

 

 

 

 

### HashMap 头插法导致死循环的原因 在 JDK 1.7 中,`HashMap` 底层采用数组加链表的数据结构来存储键值对。当发生哈希冲突时,新加入的节点会以头插法的形式插入到已有的链表头部[^4]。 #### 死循环产生的原因 假设在一个多线程环境中两个线程几乎同时触发了 `HashMap` 的扩容操作: - 当第一个线程正在进行扩容迁移过程中被调度器暂停; - 第二个线程也开始执行扩容并完成部分迁移工作; - 这时候如果第一个线程恢复运行,则它将继续按照自己保存的状态继续迁移剩余元素; 由于两次并发的操作都试图修改同一个位置上的单向链表,并且都是基于各自看到的那个时刻之前的快照来进行调整,这就可能导致某些节点形成环形引用关系,即 A->B 和 B->A 同时存在的情况,从而造成遍历时陷入无限循环之中[^2]。 ```java // 模拟JDK1.7中HashMap扩容过程中的错误场景 public class UnsafeResize { static Node<K,V>[] table; public void resize() { int oldCapacity = (table.length); Entry[] oldTable = table; int newCapacity = oldCapacity << 1; Entry[] newTable = new Entry[newCapacity]; for (int i = 0; i < oldCapacity ; i++) { for (Entry e = oldTable[i]; e != null;) { Entry next = e.next; // 计算新的索引位置... int newIndex = indexFor(e.hash, newCapacity); // 使用头插法将当前entry放到新的bucket里 e.next = newTable[newIndex]; newTable[newIndex] = e; // 移动到下一个entry e = next; } } table = newTable; } } ``` 上述代码片段展示了如何在扩容期间使用头插法更新桶内的链接列表。然而,在高竞争环境下这种做法容易引发竞态条件,进而破坏原有链表结构完整性。 ### 解决方案 为了防止此类问题的发生,可以采取以下措施之一: - **升级至更高版本**:自 JDK 1.8 开始,官方已经修复了此缺陷。新版 `HashMap` 不仅引入红黑树优化长链路查找效率,而且改变了处理碰撞的方式——由原来的单纯链表改为尾部追加模式,有效规避了因不当插入顺序而引起的异常状况。 - **同步控制访问**:虽然简单粗暴地给整个 map 加锁确实能解决问题,但这显然不是最优解因为这会影响到整体性能表现。更推荐的做法是在具体业务逻辑层面合理运用细粒度锁定机制或者选用其他支持并发读写的集合类如 `ConcurrentHashMap` 来替代普通 `HashMap` 实现相同功能需求[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值