HashMap多线程下发生死循环的原因

本文深入探讨了HashMap在多线程环境下产生死循环的原因及过程。通过具体实例演示了正常及并发下的rehash过程,解释了环形链表形成机制及其可能导致的死循环问题。

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

概述


大神陈皓已经在疫苗:JAVA HASHMAP的死循环一文中详细描述了HashMap多线程下产生死循环的原因,我仔细研读了这篇大作,做了一些笔记,加上自己的一些理解
整理出一些信息,发出来与大家交流交流。


HashMap存储的数据结构


陈皓在Hash表数据结构这一节提到了HashMap的数据结构以及扩容问题,关于这一点我之前写过的
HashMap的put和get方法原理HashMap扩容已经有详细的描述了。


多线程rehash的时候如何造成闭环链表


rehash源代码
这里写图片描述

这里写图片描述


正常的rehash过程


数据准备
在size=2的HashMap中按照顺序添加5, 7, 3这三个key,假设按照mod 2的算法来计算元素数组下标,那么key 5,7,3都会落在下标为1的数组桶中(发生hash冲突),如下图:
这里写图片描述

把HashMap的size扩容为4后,rehash的过程

注意,发生hash冲突的5,7,3虽然都是在同一个链表中,但是每个元素都得走rehash的过程,因为HashMap扩容后,这几个元素就未必都是在同一个链表中了

1、第一个是处理3这个key,先把key为3这个元素的next设置为空,并计算它在新数组中的下标,并存到新下标对应桶中,如下图:
这里写图片描述

2、第二个是处理7这个key,按照上面的约定,在新数组中3和7这个两个key还是发生了hash冲突,那么按照HashMap发生冲突的处理代码,链表的第一个元素存储的是最新插入的7,然后next指向3,如下图:
这里写图片描述

3、第三个是处理5这个key,如下图:

这里写图片描述

到这里一次正常rehash过程走完了,最后三个key的存储情况如下图:

这里写图片描述


并发下的rehash过程


当两个并发线程thread1和thread2都同时进入到transfer时,也即是,刚好thread1和thread2都要对HashMap进行扩容,万一这个时候thread1执行下面的代码时,被线程调度器挂起了,而thread2则正常的把扩容的操作做完,如下图:
这里写图片描述
那这个时候,容器的数据存储情况如下:
对于thread1
这里写图片描述

对于thread2

这里写图片描述

这个时候,thread1拥有执行权限了,则继续它的扩容操作,等thread1扩容完后就产生了一个环形链表了(注意这里省略了一些步骤,不太明白的,则可以看我之前写的HashMap的put和get方法原理HashMap扩容)

这里写图片描述

这个时候,如果有个get请求,就有可能发生死循环,一直在链表中绕来绕去的,没法终止。

### Java HashMap 多线程死循环解决方案 #### 使用 `ConcurrentHashMap` 替代 `HashMap` 为了防止在多线程环境中发生死循环和其他并发问题,推荐使用 `ConcurrentHashMap` 来替代普通的 `HashMap`。`ConcurrentHashMap` 是专门为高并发场景设计的,在执行读写操作时不会锁定整个哈希表,而是只锁定部分桶(segment),从而提高了性能并避免了死循环发生。 ```java import java.util.concurrent.ConcurrentHashMap; public class ConcurrentExample { private static final ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); public static void main(String[] args) { // 插入键值对 map.put("key", "value"); // 获取值 System.out.println(map.get("key")); } } ``` #### 同步机制保护 `HashMap` 如果确实需要继续使用 `HashMap` 而不是切换到其他更合适的类,则可以通过同步方法来确保每次访问都是原子性的。这通常意味着将所有的存取操作包裹在一个显式的锁内: ```java import java.util.HashMap; import java.util.Map; public class SynchronizedMapExample { private final Map<String, String> map = new HashMap<>(); private final Object lock = new Object(); public void put(String key, String value) { synchronized (lock) { map.put(key, value); } } public String get(String key) { synchronized (lock) { return map.get(key); } } public boolean remove(Object key) { synchronized (lock) { return map.remove(key) != null; } } } ``` 这种方法虽然能解决问题,但是会带来一定的性能开销,并且不如直接使用 `ConcurrentHashMap` 方便高效[^2]。 #### 升级至 JDK 1.8 或更高版本 自JDK 1.8起,`HashMap` 已经改进了其内部实现方式,采用了红黑树代替链表作为冲突处理的方式之一,并且解决了因头插法造成的死循环问题。因此升级到更新版本也可以有效规避此风险[^4]。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值