HashMap 为什么线程不安全?

HashMap 为什么线程不安全?HashMap 为什么线程不安全 文章目录 HashMap 为什么线程不安全 前言项目环境 1 put 方法中的 modCount 问题 2 扩容期间取值不准确 3 同时 put 碰撞导致数据丢失 4 可见性问题 5 扩容头插法可能导致的循环链表问题 6 总结 7 参考前言本文从以下几个方面来讨论 HashMap 为什么是线程不安全的 put 方法中的 modCount 问题扩容期间取值不准确同时 put 碰撞导致数据丢失可见性问题扩容头插法可能导致的循环链表问题 jdk1 8 以前版本 jd

HashMap 为什么线程不安全?

前言

本文从以下几个方面来讨论 HashMap 为什么是线程不安全的

  • put 方法中的 modCount++ 问题
  • 扩容期间取值不准确
  • 同时 put 碰撞导致数据丢失
  • 可见性问题
  • 扩容头插法可能导致的循环链表问题(jdk 1.8 以前版本)
    • jdk 1.8+ 修改为尾插法解决了这个问题

项目环境

  • jdk 1.8
  • github 地址:https://github.com/huajiexiewenfeng/java-concurrent
    • 本章模块:collection

1.put 方法中的 ++modCount 问题

jdk1.8 – put 方法源码如下,省略了部分代码:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 
    Node<K,V>[] tab; Node<K,V> p; int n, i; ... ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 

++modCount 是一个复合操作,也是非常经典的线程不安全代码,我们可以自己写了一个例子

public class NotSafeThreadIncrementDemo { 
    static int i = 0; static int j = 0; public static void main(String[] args) { 
    for (int k = 0; k < 100; k++) { 
    new Thread(() -> { 
    i++; ++j; }).start(); } System.out.printf("变量 i 的值:[%d]\n变量 j 的值:[%d]\n", i, j); } } 

执行结果:

变量 i 的值:[97] 变量 j 的值:[98] 

预期结果是 i=j=100,但是实际上每次执行的结果都不一样,甚至 i 和 j 的值都不一定相等。

原因如下:

从表面上看 i++ 只是一行代码,但实际上它并不是一个原子操作,它的执行步骤主要分为三步,而且在每步操作之间都有可能被打断。

  • 第一步:读取
  • 第二步:增加
  • 第三步:保存

图解:
在这里插入图片描述
假设 线程1、线程2 两个线程同时进行 i++ 的操作如上图所示,我们按照箭头的方向进行说明




为啥线程会被切换走?

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方法来实现的,在任何一个确定的时刻,一个处理器(对于多核来说是一个内核)都之会执行一条线程中的指令。

–《深入理解Java虚拟机》周志明 (第二章-运行时数据区中->程序计数器相关的描述)

线程 1 先执行拿到 i=1,第二步进行 i+1 的操作,此时应该进行第三步 i=2 的操作,但是 线程 1 被切换走了,线程 2 抢到了 CPU 的时间片, 开始执行拿到 i=1(因为线程1的 i=2 的值还没有保存,所以线程2拿到的 i 的值还是为 1),进行第四步 i+1 的操作,然后按照箭头继续后面 5,6,7 操作,最终我们得到 i 的值为 2,并不是预期的 3,这就是线程安全问题。

由此可以得出 HashMap 是线程非安全的,如果有多个线程同时调用 put 方法,++modCount 的值就有可能计算错误。

2.扩容期间取值不准确

首先我们来解释一下这个问题,HashMap 在数据不断添加的过程中,在一定条件下会触发扩容的机制,在扩容期间,会新建一个新的空数组,并用旧的元素填充到新数组中。在这个填充的过程中,如果有线程去取值,就有可能取到 null 值。

我们来看下面这个示例代码

  • 循环 1000 次 map 中添加元素(次数不够可以增加,主要是为了触发扩容机制)
  • 预先设置一个值 key=1000,value=“xwf”
  • 不断的去获取我们预先设置的值,看 value 值是否为空,如果为空就表示,在扩容过程中发生了数据不一致问题
public class NotSafeHashMapDemo { 
    public static void main(String[] args) throws InterruptedException { 
    final Map<Integer, String> map = new HashMap<>(); final Integer targetKey = 1000;// 如果不出异常,可以增大这个值 final String targetValue = "xwf"; map.put(targetKey, targetValue); new Thread(() -> { 
    IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue")); }).start(); // 不断的循环取 targetKey 对应的值,预期值为 xwf while (true) { 
    System.out.println(map.size());// 查看 map 集合大小 if (null == map.get(targetKey)) { 
   // 如果取到的值为 null 表示在扩容的过程中,原来 targetKey 的值发生了变化 throw new RuntimeException("HashMap is not thread safe."); } else { 
    System.out.println(map.get(targetKey)); } } } } 

执行结果:

... 集合的大小:637 xwf 集合的大小:661 xwf 集合的大小:703 xwf 集合的大小:738 xwf 集合的大小:768 Exception in thread "main" java.lang.RuntimeException: HashMap is not thread safe. at com.csdn.collection.NotSafeHashMapDemo.main(NotSafeHashMapDemo.java:30) 

当集合大小到 768 的时候,发生了取值异常的情况,验证了我们的结论。

3.同时 put 碰撞导致数据丢失

但是在多线程环境下有可能发生如下情况,两个线程同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。

4.可见性问题

可见性也是线程安全的一部分,当一个线程操作这个容器的时候,该操作需要对另外的线程都可见,也就是其他线程都能感知到本次操作。可是 HashMap 对此是做不到的,如果 线程1 给某个 key 放入了一个新值,那么 线程2 在获取对应的 key 的值的时候,它的可见性是无法保证的,也就是说 线程2 可能可以看到这一次的更改,但也有可能看不到。所以从可见性的角度出发,HashMap 同样是线程非安全的。

5.扩容头插法可能导致的循环链表问题

在 JDK1.8 之前,HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

6.总结

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中如果需要使用 Map,应该尽量避免使用线程不安全的 HashMap。同时,虽然 Collections.synchronizedMap(new HashMap()) 是线程安全的,但是效率低下,因为内部用了很多的 synchronized,多个线程不能同时操作。推荐使用线程安全同时性能比较好的 ConcurrentHashMap。

博主之前有一篇文章对 ConcurrentHashMap 进行了相关介绍:

Java并发编程|第十篇:ConcurrentHashMap源码分析

友情提醒,源码分析较多,有兴趣可以了解。

7.参考

  • 《深入理解Java虚拟机》- 周志明
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/227628.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午8:52
下一篇 2026年3月16日 下午8:52


相关推荐

  • 腾讯元宝更新,多图上传+智能处理一键搞定

    腾讯元宝更新,多图上传+智能处理一键搞定

    2026年3月12日
    2
  • 使用sortablejs拖拽列表排序

    使用sortablejs拖拽列表排序sortablejs 版本 1 14 0 框架 vue UI elementimpor sortablejs 引入开始使用 我的接口 请求表格数据 fetchData getUser then res gt this list res data data this total res data count this nextTick gt 表格渲染完成 调用拖拽排序

    2026年3月19日
    3
  • 产品流量分析

    产品流量分析年底要接的数据需求好多,博客好久没更新了。这次和大家分享一下最近对流量分析的一些理解。流量是产品获得用户的第一步,没有流量就没有转化与营收。对于流量的分析在产品日常运营效果监控中有着非常重要意义。下面我们就流量的来源与流向分析中需要关注哪些指标,展开叙述。这里首先放一张对流量来源和去向的图:从流量来源角度来看,其来源包括直接访问、搜索访问、商务合作以及自媒体等方面:直接访问:用户直…

    2022年6月2日
    38
  • pycharm常用快捷键详解,让你编程 事半功倍。[通俗易懂]

    pycharm常用快捷键详解,让你编程 事半功倍。[通俗易懂]pycharm常用快捷键1、编辑(Editing)Ctrl+Space:基本的代码完成(类、方法、属性)Ctrl+Alt+Space快速导入任意类Ctrl+Shift+Enter:语句完成Ctrl+P参数信息(在方法中调用参数)Ctrl+Q快速查看文档F1外部文档Shift+F1:外部文档,进…

    2022年8月25日
    17
  • 三Agent协同实战:用OpenClaw+星链4SAPI搭了个写作流水线,省了70%成本

    三Agent协同实战:用OpenClaw+星链4SAPI搭了个写作流水线,省了70%成本

    2026年3月14日
    2
  • skiplist 跳跃表详解及其编程实现

    skiplist 跳跃表详解及其编程实现skiplist 介绍跳表 skipList 是一种随机化的数据结构 基于并联的链表 实现简单 插入 删除 查找的复杂度均为 O logN 跳表的具体定义 请参考参考维基百科点我 中文版 跳表是由 WilliamPugh 发明的 这位确实是个大牛 搞出一些很不错的东西 简单说来跳表也是链表的一种 只不过它在链表的基础上增加了跳跃功能 正是这个跳跃的功能 使得在查找元素时 跳表能

    2026年3月26日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号