HashMap多线程扩容导致死循环解析(JDK1.7)

本文详细解释了在JDK1.7中,当两个线程并发扩容HashMap时,由于头插法操作可能导致链表顺序反转,从而引发死循环的情况。讨论了问题发生的条件和解决思路。
HashMap多线程扩容导致死循环解析(JDK1.7)

先上源码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            // 1.保存下一次循环要操作的元素
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            // 2.采用头插法将e元素插入到链表头部
            e.next = newTable[i];
            newTable[i] = e;
            // 3.遍历到下一个元素
            e = next;
        }
    }
}

前提:

  1. 两个线程同时扩容同一个hashmap,并且两个线程都已经执行到获取第一个元素的next(步骤1)
  2. 前两个元素重新计算下标后没有改变位置
  3. 第一个线程将扩容执行完毕,另一个线程才开始下一步操作

由于头插法的原因,再次执行头插法的时候会将原来的链表顺序反过来,所以当第一个线程执行完扩容之后会将顺序从

1->2->3 到 3->2->1

线程二第一次执行插入结束后

请添加图片描述

线程二准备第二次循环结束后
在这里插入图片描述

线程二第二次执行插入结束后

请添加图片描述

线程二准备第二次循环结束后
在这里插入图片描述

线程二第三次执行插入结束后
在这里插入图片描述

线程二准备第三次循环结束后
在这里插入图片描述

最后由于e等于null,退出循环,当下次遍历到该元素时就会进入死循环

<think>嗯,用户想知道JavaHashMap多线程环境下的问题及原因,特别是JDK1.7死循环JDK1.8的数据覆盖,还有解决方案。首先我得回忆一下HashMap在不同版本的结构差异。 JDK1.7用的是数组加链表,插入链表的时候是头插法,这样在扩容时可能会导致死循环。而JDK1.8改成了尾插法,并引入了红黑树,但数据覆盖的问题还存在。需要分版本解释问题。 然后,用户可能实际遇到过并发问题,或者在面试中被问到,所以需要详细说明原因。比如JDK1.7扩容时的transfer方法,多线程同时操作链表,导致节点指向混乱,形成环形链表,进而死循环。这个得结合源码步骤分析,比如Entry的next指针被修改的顺序。 对于JDK1.8,虽然解决了死循环,但put操作时如果没有同步,多个线程可能同时计算哈希、找到桶位置,然后覆盖彼此的数据,造成数据丢失。这时候需要说明具体如何覆盖,比如检查到桶为空,然后同时插入。 解决方案的话,用户可能想知道如何避免这些问题,比如使用ConcurrentHashMap或者Collections.synchronizedMap,或者改用Hashtable。需要对比不同方案的优缺点,比如ConcurrentHashMap的锁分段技术,性能更好。 还要注意引用给出的资料,比如引用1、3、5提到JDK1.7死循环和数据丢失,引用4和5说明JDK1.8的优化和仍然存在的问题。引用2提到JDK1.8的红黑树改进。需要正确标注引用来源。 用户可能更关注实际应用中的解决方案,所以需要给出明确的建议,比如在高并发下推荐使用ConcurrentHashMap。同时,可能希望了解背后的原理,比如为什么JDK1.8的改动解决了死循环,但仍有数据覆盖。 最后,确保回答结构清晰,分点说明问题、原因和解决方案,并生成相关问题,帮助用户进一步学习。比如问到JDK1.8的优化细节,或者ConcurrentHashMap的实现原理。</think>### Java HashMap多线程问题及原因分析 #### 一、JDK1.7中的多线程问题 1. **环形链表与死循环** - **原因**:JDK1.7扩容方法`transfer()`使用**头插法**迁移链表节点。在多线程并发扩容时,若线程A和线程B同时操作链表,可能导致链表节点引用形成环形结构(例如,线程A的`Entry.next`被线程B修改后指向已迁移的节点)。 - **后果**:后续调用`get()`方法时,若查询的键恰好落在环形链表所在的桶,会触发无限循环,导致CPU占用率飙升[^3][^5]。 - **源码示例**: ```java void transfer(Entry[] newTable) { for (Entry<K,V> e : table) { while (null != e) { Entry<K,V> next = e.next; // 步骤1:保存下一个节点 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; // 步骤2:头插法插入新表 newTable[i] = e; // 步骤3:更新表头 e = next; // 步骤4:遍历下一个节点 } } } ``` 若两个线程同时执行上述代码,可能导致`e.next`指向错误节点,形成环形链表。 2. **数据丢失** - **原因**:多个线程同时执行扩容操作时,可能因竞争导致部分节点未被正确迁移到新数组,最终被垃圾回收[^1]。 --- #### 二、JDK1.8中的多线程问题 1. **数据覆盖** - **原因**:JDK1.8在`putVal()`方法中,若多个线程同时判断某个桶为空,可能先后写入不同的键值对,导致后写入的值覆盖前一个。例如: ```java if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 多个线程可能同时执行此操作 ``` - **后果**:最终只有一个线程的数据被保留,其他数据丢失。 2. **红黑树操作的竞争** - **原因**:JDK1.8在链表转红黑树时,若多个线程同时操作,可能破坏红黑树的平衡性,导致查询结果异常(尽管概率较低)[^2]。 --- #### 三、解决方案 1. **使用线程安全的替代类** - **ConcurrentHashMap**(推荐):JDK1.7采用分段锁,JDK1.8改为CAS+synchronized,保证并发安全[^4]。 - **Hashtable**:通过全表锁实现线程安全,但性能较差。 - **Collections.synchronizedMap()**:对HashMap包装一层同步控制。 2. **避免多线程直接操作HashMap** - 若必须使用HashMap,需通过外部锁(如`synchronized`)保证原子性。 --- #### 四、版本差异对比 | 特性 | JDK1.7 | JDK1.8 | |---------------------|---------------------------|---------------------------------| | 数据结构 | 数组+链表 | 数组+链表+红黑树 | | 插入方式 | 头插法 | 尾插法 | | 扩容问题 | 死循环、数据丢失 | 数据覆盖 | | 优化点 | - | 红黑树优化哈希冲突性能 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛先森*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值