多线程下HashMap死循环问题源码分析

 欢迎大家关注公众号“爪哇缪斯”\(^o^)/~ 「干货分享,每周更新」

在JDK8之前,当我们采用多线程的方式向HashMap中插入元素的时候,会有一定的概率造成线程死循环。这个问题在面试中也是比较常见的,那么原因是什么呢?“面试宝典”里面常常会给出如下极简的答案:“在数据迁移过程中,因为会采用头插法,所以会造成多线程死循环。而jDK8之后(包含8)则采用了尾插法,所以,可以有效的避免这个问题”。那么,本篇小短文就带着大家来到JDK7的源码中去深入的寻找更完整的答案。

put方法基本流程

首先,判断table数组是否为空(即:{}),如果为空,则调用inflateTable(threshold)方法初始化一个默认长度为16的数组。源码如下所示:

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

其次,如果key等于null,则将其放入table[0]所在元素的链表中。源码如下所示:

if (key == null) {
    return putForNullKey(value);
}

第三,通过key进行rehash操作,计算出待插入到table数组中的位置i,如果这个元素之前插入过,则更新value值,并将旧的value值返回出去。源码如下所示:

int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }
}

最后,如果这个元素没插入过,则调用addEntry进行添加。源码如下所示:

addEntry(hash, key, value, i);

addEntry(hash, key, value, i);

addEntry方法中,会涉及到table数组扩容&数据迁移操作。那么在这个场景下,我就可以看到多线程下如何会造成死循环。

相关的代码就在addEntry方法中的resize(2 * table.length)方法里,如下所示:

13ba2c334daa63e080358aee101f4bb7.png数据迁移逻辑概述

数据迁移真正逻辑就在transfer(Entry[] newTable, boolean rehash) 方法中,源码和注释如下所示:

当然,这么看起来不是那么直观,下面我会以图示的方式演示如何数据迁移的。

首先,我们需要知道的知识点就是,JDK7中采取的是头插法进行数据迁移,那么迁移后的新旧链表顺序其实就是相反的。如下图所示:

数据迁移详解

在上一节内容中,我们已经看到了transfer方法的源码,那么下面我们就根据源码的内容,演示每一步的数据迁移操作。迁移的场景就是原有数组下标0处有一条链表A->B->C,对其进行迁移。迁移详细步骤如下所示:

从上图中,已经演示了如何通过transfer方法中关键的代码内容执行数据迁移了。

多线程死循环场景

既然数据迁移的整个过程我们已经介绍过了,那么还是假设一个场景:有线程A和线程B这两条线程。同时对HashMap进行数据迁移操作。那么,线程A是正常执行的,线程B执行过程中慢了些。那么为何会产生死循环呢?详情请看下图:

总结

如上的代码是针对于JDK7的解析,从JDK8开始,HashMap源码改进的内容还是蛮大的,从rehash的方式再到加入红黑树再到尾插法等等。

所以,在面试过程中,面试官想要考察面试者是否有看源码的习惯或者能力时,都会考察变更前和变更后的区别。当然,本篇文章只是举了一个特定的例子,会造成死循环,由于HashMap不是线程安全的,所以在多线程场景下问题还是比较多的。

如果想要更加详细的了解非线程安全的JDK8版本的HashMap源码解析,请跳到公众号【爪哇缪斯】搜索这篇文章。

<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 | |---------------------|---------------------------|---------------------------------| | 数据结构 | 数组+链表 | 数组+链表+红黑树 | | 插入方式 | 头插法 | 尾插法 | | 扩容问题 | 死循环、数据丢失 | 数据覆盖 | | 优化点 | - | 红黑树优化哈希冲突性能 | ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值