hashmap为什么线程不安全

2025-04-1503:46:48综合资讯0

对于HashMap,很多人可能已经知道它在多线程环境下的使用隐患,但具体细节或许还不太清楚。关于这个问题,其实很早以前,在淘宝的内网中就有程序员讨论过,他们遇到过因为多线程环境下使用HashMap导致的死循环问题,CPU使用率一度飙升至100%。

很多面试时,面试官会问及是否了解HashMap在多线程环境下的这种潜在问题。实际上,这种情况确实存在,并且已经发生多次。虽然Java官方明确表示,在多线程环境下不推荐使用HashMap,但对于不了解这一点的开发者来说,可能会意外遇到这种问题。

那么,为什么会发生死循环呢?下面我们来详细解析一下问题的来龙去脉。

HashMap是一个哈希数组加链表的数据结构,在日常的程序开发中经常用到。如果大家对HashMap的详细实现原理还不太了解,可以先看一些相关的解析文章。特别要指出的是,HashMap是一个非线程安全的集合操作类,在单线程环境下使用是没有问题的。当多个线程同时操作时,就会出现问题。

我们可以通过一个简单的测试来复现这个问题。使用四个线程向HashMap中添加元素,可能一次运行不会出现问题,可以多次运行试试。

从控制台输出的结果可以看到,在遍历map的内容时,已经出现了死循环。通过活动器可以看到CPU的使用率接近200%。

查看刚刚运行的HashThreadTest类的堆栈情况,可以看到HashMap的扩容操作导致了死循环。

通过测试,我们发现在多线程环境下操作HashMap,确实会产生死循环,并导致CPU使用率100%。

那么,为什么会这样呢?让我们一起深入研究一下HashMap的源码。

在进行测试的时候,使用的是JDK1.7版本。如果你使用的是JDK1.8版本,可能无法复现这个问题,因为JDK1.8已经修复了这个问题,但即便如此,依然不建议在多线程环境下使用HashMap。

接下来,我们来看看为什么使用JDK1.7会出现这个问题。既然问题出在put阶段,我们不如来看一下HashMap的put过程。

HashMap的put源码实现如下:接着是addEntry()方法,将元素插入到数组中,并检查容量是否超标。源码实现如下:

在上面的例子中,我们初始化时给定的容量是2,所以在添加元素时一定会进行扩容。如果超出阈值,就进行扩容处理,创建一个更大容量的hash表,然后将旧的hash表中的元素迁移到新的hash表中。源码如下:

整个put过程大致可以分为以下几个步骤:

1. 通过key计算出的hash值和equals来判断元素是否存在,如果存在则直接覆盖,否则插入;

2. 将元素插入到hash表中,如果不同的元素都在同一个hash数组下标下,则以链表的形式存储;

3. 判断当前数组容量是否大于扩容阈值,如果大于则进行扩容处理,然后将旧元素复制到新的数组中;

这个过程基本上没有问题。接下来我们演示一下扩容中重新计算元素hash的过程。假设在单线程环境下,我们初始化时给定的数组容量是2,分别添加三个元素。源码如下:添加完成后,数组会进行扩容处理,扩容后hash的容量为原来的两倍。接下来我们看一下并况下的扩容过程。假设有两个线程分别添加三个元素。在第二个线程执行完添加任务准备迁移旧元素到新元素时,突然被CPU挂起。而此时第一个线程已经完成扩容。当第二个线程被唤醒后继续执行时,就会出现问题。详细的分析过程如下:通过源码中的循环过程可以看到,在遍历hashMap的链表内容时会出现死循环。因此当我们在多线程环境下使用HashMap时就会出现上文中提到的问题:死循环以及CPU使用率飙升。对于这个问题当初有人上报到SUN公司但SUN并不认为这是问题因为HashMap本来就不支持并发操作!因此不建议在多线程环境下使用HashMap那如果要在多线程环境下使用map操作类该怎么办呢?办法肯定是有的大家可以在多线程场景下使用HashMap时采用以下两种解决办法:第一种是推荐使用并发包中的ConcurrentHashMap类一种使用分段锁的hashMap类;另一种是使用Collections.synchronizedMap方法将普通的Map对象转化为同步的Map对象但需要注意这种方式的性能可能不如ConcurrentHashMap。总之对于多线程环境下的并发问题我们需要谨慎对待选择合适的工具和方法来避免潜在的问题发生。