多线程下hashmap的死链问题

本文解析了JDK1.7中HashMap头插法引发死循环的机制,并介绍了尾插法则避免死循环的原因,以及如何选择线程安全替代品。

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

多线程情况下HashMap的头插法和尾插法,为什么尾插法会避免死循环?

我觉得尾插法也会出现环形链表的问题,为什么网上直说头插法存在这样的问题,多线程情况下就是会出现指针指向问题,网上大多数都讲的不是很清楚。

1、数据插入原理

在分析原因之前,我先带大家了解一下JDK1.7中HashMap插入数据的原理,来看动画演示:

由于JDK 1.7中HashMap的底层存储结构采用的是数组 加 链表的方式。

 

而HashMap在数据插入时又采用的是头插法,也就是说新插入的数据会从链表的头节点进行插入。

 

 

因此,HashMap正常情况下的扩容就是是这样一个过程。我们来看,旧HashMap的节点会依次转移到新的HashMap中,旧HashMap转移链表元素的顺序是A、B、C,而新HashMap使用的是头插法插入,所以,扩容完成后最终在新HashMap中链表元素的顺序是C、B、A

2、导致死循环的原因

接下来,我通过动画演示的方式,带大家彻底理解造成HashMap死循环的原因。我们按以下三个步骤来还原并发场景下HashMap扩容导致的死循环问题:

 

 

第一步:线程启动,有线程T1和线程T2都准备对HashMap进行扩容操作, 此时T1和T2指向的都是链表的头节点A,而T1和T2的下一个节点分别是T1.next和T2.next,它们都指向B节点。 

 

第二步:开始扩容,这时候,假设线程T2的时间片用完,进入了休眠状态,而线程T1开始执行扩容操作,一直到线程T1扩容完成后,线程T2才被唤醒。

 

T1完成扩容之后的场景就变成动画所示的这样。

 

因为HashMap扩容采用的是头插法,线程T1执行之后,链表中的节点顺序发生了改变。但线程T2对于发生的一切还是不可知的,所以它指向的节点引用依然没变。如图所示,T2指向的是A节点,T2.next指向的是B节点。

 

 

 

当线程T1执行完成之后,线程T2恢复执行时,死循环就发生了。

因为T1执行完扩容之后,B节点的下一个节点是A,而T2线程指向的首节点是A,第二个节点是B,这个顺序刚好和T1扩容之前的节点顺序是相反的。T1执行完之后的顺序是B到A,而T2的顺序是A到B,这样A节点和B节点就形成了死循环。 

3、解决方案

避免HashMap发生死循环的常用解决方案有三个:
1)、使用线程安全的ConcurrentHashMap替代HashMap,个人推荐使用此方案。

2)、使用线程安全的容器Hashtable替代,但它性能较低,不建议使用。

3)、使用synchronized或Lock加锁之后,再进行操作,相当于多线程排队执行,也会影响性能,不建议使用。

4、总结

HashMap死循环只发生在JDK1.7版本中,主要原因是JDK1.7中的HashMap,在头插法 加 链表 加 多线程并发 加 扩容这几个情形累加到一起就会形成死循环。多线程环境下建议采用ConcurrentHashMap替代。在JDK1.8中,HashMap改成了尾插法,解决了链表死循环的问题。

### Java HashMap 导致循环的原因 在 JDK 1.7 中,`HashMap` 的底层实现采用的是 **数组 + 链表** 结构[^3]。当发生扩容时,原有的桶中的节点会被重新分配到新数组的不同位置。如果此时有多个线程同时对 `HashMap` 进行写操作(如插入或删除),可能会引发竞态条件。 具体来说,在多线程环境中,某个线程可能正在执行扩容逻辑,而其他线程也在尝试修改同一个 `HashMap` 实例。由于 `HashMap` 并未提供内置的线程安全性机制,这种情况下可能发生以下问题: - 扩容过程中,某些节点被错误地接成环形链表[^2]。 - 当后续调用 `get` 方法遍历该链表时,程序会陷入无限循环,从而导致 CPU 占用率飙升甚至系统崩溃。 #### 解决方案 为了防止因 `HashMap` 多线程访问而导致的循环或其他一致性问题,可以采取以下几种方法之一来替代原始的 `HashMap` 使用方式: 1. **使用 `Hashtable` 替代 `HashMap`:** - `Hashtable` 是一种古老的集合类,其内部通过 synchronized 关键字实现了所有公共方法的同步控制,因此它是线程安全的[^1]。 2. **利用 `Collections.synchronizedMap()` 包装器:** - 可以通过对普通的 `HashMap` 应用此静态工厂方法创建一个线程安全版本的地图对象。需要注意的是,虽然返回的对象本身具备一定程度上的同步保护功能,但在迭代期间仍然需要显式锁定整个映射实例才能确保绝对的安全性。 3. **推荐使用 `ConcurrentHashMap`:** - 自 JDK 1.5 开始引入的一种高度优化过的并发哈希表实现形式。它采用了分段锁技术(segments),允许更高的并行度以及更少的竞争开销相比传统的完全互斥锁策略而言更为高效;另外自JDK8起改用了红黑树代替部分场景下的链表结构进一步提升了性能表现。 以下是基于以上三种不同解决办法的一个简单代码示例展示如何正确处理这种情况: ```java // 方案一:使用 Hashtable import java.util.Hashtable; public class Example { public static void main(String[] args){ Hashtable<Integer, String> hashtable = new Hashtable<>(); hashtable.put(1,"one"); } } // 方案二:使用 Collections.synchronizedMap() import java.util.Collections; import java.util.HashMap; public class ExampleSync{ private final Map<String,Integer> map=Collections.synchronizedMap(new HashMap<>()); public Integer get(String key){return map.get(key);} public void put(String key,int value){map.put(key,value);} } // 方案三:使用 ConcurrentHashMap (推荐) import java.util.concurrent.ConcurrentHashMap; public class ExampleCHM { private final ConcurrentHashMap<Long, Object> chm=new ConcurrentHashMap<>(); public boolean tryCache(Object obj){ return null==chm.putIfAbsent(System.identityHashCode(obj),obj); } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值