为什么HashMap的线程不安全性

你好,我是风一样的树懒,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读


前一篇我们分享的《JDK 1.7 vs JDK 1.8 中 HashMap 的区别》可以看看,下一篇继续分享相关内容


HashMap 的线程不安全性主要体现在多个线程并发修改同一个 HashMap 实例时,可能会导致数据不一致、死循环、数据丢失等问题。下面是详细的体现及解释:

01
线程不安全的表现


数据不一致

当多个线程同时对 HashMap 进行修改(插入、删除或更新),如果没有进行适当的同步,会导致数据的不一致性。例如,一个线程正在执行插入操作,而另一个线程正在读取该数据,可能导致读取的数据不正确,甚至出现丢失或错乱的情况。


死循环问题

这是 HashMap 在并发修改下非常严重的一个问题,尤其是在 JDK 1.7 版本及之前的实现中。当多个线程并发地插入元素,可能导致链表结构发生变化,链表节点可能会被重新排序,如果不采取适当的措施,可能会导致环形链表结构的形成。这种情况下,遍历该 HashMap 会进入 死循环。

数据丢失

在多线程并发插入操作时,HashMap 可能会丢失数据,尤其是在多线程同时执行 put() 操作时。由于 HashMap 不是线程安全的,两个线程可能会同时写入相同的 hash 值的桶中,导致其中一个线程的写入被覆盖,最终造成数据丢失。

结构不一致(resize 期间的错误)

在 HashMap 扩容时,底层数组会重新分配内存并将现有数据迁移到新数组。当多个线程同时访问 resize() 操作时,可能导致数据丢失或者结构不一致。具体来说,当两个线程同时触发扩容时,可能会出现数组的索引计算错误,导致一些数据丢失或者覆盖。


02
线程不安全的例子


下面是一个简单的并发问题示例:

import java.util.HashMap;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class HashMapConcurrencyExample {    public static void main(String[] args) {        HashMap<StringString> map = new HashMap<>();        ExecutorService executor = Executors.newFixedThreadPool(2);        executor.submit(() -> {            for (int i = 0; i < 1000; i++) {                map.put("key" + i, "value" + i);            }        });        executor.submit(() -> {            for (int i = 0; i < 1000; i++) {                map.put("key" + i, "newValue" + i);            }        });        executor.shutdown();    }}

在这个例子中,两个线程同时执行 put() 操作,将数据写入 HashMap。由于没有同步机制,可能会发生以下几种情况:

  • 数据丢失(第二个线程可能覆盖第一个线程插入的数据)。

  • 错误的大小或元素被丢失。

  • 可能会引发并发问题。


03
线程不安全的原因


HashMap 在多线程并发访问时会存在问题,原因在于它的操作(如 put()、remove())是非原子的。具体来说:

  • 读取操作 (get()) 在并发情况下,数据是可以被修改的,但读取的过程本身是无锁的。

  • 修改操作 (put()) 并非原子操作,HashMap 在内部执行插入、删除、更新时需要多个步骤,这些步骤没有适当的同步机制来保证线程安全。

例如,put() 操作实际上是三步操作:

  • 计算哈希值,确定存放位置。

  • 如果目标位置已存在元素,则链接在链表或红黑树中。

  • 更新数组位置的引用。

如果多个线程同时执行这些步骤,可能会导致其中一个线程的插入被覆盖或出错。


04
解决线程不安全的方法


使用 ConcurrentHashMap

ConcurrentHashMap 是专门为并发设计的线程安全的 Map 实现,它通过分段锁技术(JDK 1.7 及之前)和其他优化措施(JDK 1.8 中采用了 CAS 和无锁算法)来保证并发情况下的线程安全。ConcurrentHashMap 在并发访问时,不会发生死循环、数据丢失和不一致等问题。

import java.util.concurrent.ConcurrentHashMap;ConcurrentHashMap<StringString> map = new ConcurrentHashMap<>();


使用 Collections.synchronizedMap()

通过 Collections.synchronizedMap() 包装 HashMap,将其转化为线程安全的 Map。但是,尽管这样可以保证对 Map 的访问是同步的,但该实现会导致性能下降,因为整个 Map 的访问会被锁住。

import java.util.Collections;import java.util.Map;import java.util.HashMap;Map<StringString> syncMap = Collections.synchronizedMap(new HashMap<>());


显式同步操作

如果确实需要使用 HashMap,可以在使用 put()、get() 等操作时,显式地使用 synchronized 关键字,保证线程间的同步。然而,显式同步会降低并发性能,并不推荐作为常规做法。

Map<StringString> map = new HashMap<>();synchronized (map) {    map.put("key""value");}


使用 ReadWriteLock

对于频繁读、少量写的场景,可以使用 ReadWriteLock 来实现优化。通过读写锁,多个线程可以并发读取,但是写操作会互斥。


05
总结


HashMap 的线程不安全性体现在以下几个方面:

  • 数据不一致、数据丢失、死循环等问题。

  • HashMap 在并发操作时没有同步机制,导致多线程访问时可能出错。

解决方案:

  • 推荐使用 ConcurrentHashMap,它是线程安全的并发容器。

  • 如果使用 HashMap,则可以通过 synchronizedMap 或手动加锁等方式保证线程安全,但性能较差。

因此,对于大多数并发场景,ConcurrentHashMap 是更加优雅和高效的解决方案。

今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海

喜欢此内容的人还喜欢

《Java面试题指南》回归啦~


一个阿里二面面试官必问的问题


Lambda表达式说爱你不容易


分享面试:mysql数据库索引失效的情况


Spring-Boot中一个不起眼的好工具StopWatch