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

本文深入探讨了HashMap在多线程环境下可能出现的死循环、元素丢失等问题,并提供了避免这些问题的方法。

由于在公司项目中偶尔会遇到HashMap死循环造成CPU100%,重启后问题消失,隔一段时间又会反复出现。今天在这里来仔细剖析下多线程情况下HashMap所带来的问题:

1、多线程put操作后,get操作导致死循环。

2、多线程put非null元素后,get操作得到null值。

3、多线程put操作,导致元素丢失。

死循环场景重现

下面我用一段简单的DEMO模拟HashMap死循环:

复制代码
 1 public class Test extends Thread
 2 {
 3     static HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(2);
 4     static AtomicInteger at = new AtomicInteger();
 5     
 6     public void run()
 7     {
 8         while(at.get() < 100000)
 9         {
10             map.put(at.get(),at.get());
11             at.incrementAndGet();
12         }
13     }
复制代码

其中map和at都是static的,即所有线程所共享的资源。接着5个线程并发操作该HashMap:

复制代码
 1 public static void main(String[] args)
 2      {
 3          Test t0 = new Test();
 4          Test t1 = new Test();
 5          Test t2 = new Test();
 6          Test t3 = new Test();
 7          Test t4 = new Test();
 8          t0.start();
 9          t1.start();
10          t2.start();
11          t3.start();
12          t4.start();
13      }
复制代码

反复执行几次,出现这种情况则表示死循环了:

接下来我们去查看下CPU以及堆栈情况: 

通过堆栈可以看到:Thread-3由于HashMap的扩容操作导致了死循环。

正常的扩容过程

我们先来看下单线程情况下,正常的rehash过程

1、假设我们的hash算法是简单的key mod一下表的大小(即数组的长度)。

2、最上面是old hash表,其中HASH表的size=2,所以key=3,5,7在mod 2 以后都冲突在table[1]这个位置上了。

3、接下来HASH表扩容,resize=4,然后所有的<key,value>重新进行散列分布,过程如下:

 

 

 

在单线程情况下,一切看起来都很美妙,扩容过程也相当顺利。接下来看下并发情况下的扩容。

并发情况下的扩容

1、首先假设我们有两个线程,分别用红色和蓝色标注了。

2、扩容部分的源代码:

复制代码
 1 void transfer(Entry[] newTable) {
 2         Entry[] src = table;
 3         int newCapacity = newTable.length;
 4         for (int j = 0; j < src.length; j++) {
 5             Entry<K,V> e = src[j];
 6             if (e != null) {
 7                 src[j] = null;
 8                 do {
 9                     Entry<K,V> next = e.next;
10                     int i = indexFor(e.hash, newCapacity);
11                     e.next = newTable[i];
12                     newTable[i] = e;
13                     e = next;
14                 } while (e != null);
15             }
16         }
17     }
复制代码

3、如果在线程一执行到第9行代码就被CPU调度挂起,去执行线程2,且线程2把上面代码都执行完毕。我们来看看这个时候的状态:

 

 

4、接着CPU切换到线程一上来,执行8-14行代码,首先安置3这个Entry:

 

这里需要注意的是:线程二已经完成执行完成,现在table里面所有的Entry都是最新的,就是说7的next是3,3的next是null;现在第一次循环已经结束,3已经安置妥当。看看接下来会发生什么事情:

1、e=next=7;

2、e!=null,循环继续

3、next=e.next=3

4、e.next 7的next指向3

5、放置7这个Entry,现在如图所示:

放置7之后,接着运行代码:

1、e=next=3;

2、判断不为空,继续循环

3、next= e.next  这里也就是3的next 为null

4、e.next=7,就3的next指向7.

5、放置3这个Entry,此时的状态如图: 

这个时候其实就出现了死循环了,3移动节点头的位置,指向7这个Entry;在这之前7的next同时也指向了3这个Entry。

代码接着往下执行,e=next=null,此时条件判断会终止循环。这次扩容结束了。但是后续如果有查询(无论是查询的迭代还是扩容),都会hang死在table【3】这个位置上。现在回过来看文章开头的那个Demo,就是挂死在扩容阶段的transfer这个方法上面。

出现上面这种情况绝不是我要在测试环境弄一批数据专门为了演示这种问题。我们仔细思考一下就会得出这样一个结论:如果扩容前相邻的两个Entry在扩容后还是分配到相同的table位置上,就会出现死循环的BUG。在复杂的生产环境中,这种情况尽管不常见,但是可能会碰到。

多线程put操作,导致元素丢失

 下面来介绍下元素丢失的问题。这次我们选取3、5、7的顺序来演示:

1、如果在线程一执行到第9行代码就被CPU调度挂起:

2、线程二执行完成:

3、这个时候接着执行线程一,首先放置7这个Entry:

4、再放置5这个Entry:

5、由于5的next为null,此时扩容动作结束,导致3这个Entry丢失。

其他

这个问题当初有人上报到SUN公司,不过SUN不认为这是一个问题。因为HashMap本来就不支持并发。

如果大家想在并发场景下使用HashMap,有两种解决方法:

1、使用ConcurrentHashMap。

2、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap变成一个线程安全的Map。

### 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); } } ``` ###
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值