为什么 HashMap 是线程不安全的?

一、概述

HashMap 是 Java 中最常用的散列表实现,但它 并不是线程安全的。在多线程环境下,如果多个线程同时操作 HashMap(特别是执行 put() 操作),可能会出现如下问题:

  • 数据被覆盖(覆盖写)
  • 数据丢失(丢键值对)
  • 链表形成死循环(JDK 1.7 中 resize)

二、覆盖写是怎么发生的?

背景:

put(K key, V value) 时,HashMap 会:

  1. 计算键的 hash 值
  2. 定位到数组的某个桶(bucket)
  3. 遍历该桶链表,看是否存在该 key
  4. 如果不存在,就创建新节点插入链表头部

问题发生:

假设两个线程 T1T2 同时调用 put(k1, v1),都计算出相同的 hash 值(即冲突),定位到同一个桶。

  • T1 检查当前桶链表,没有发现 k1,准备插入新节点 A
  • T2 也同时判断没有 k1,准备插入新节点 B

最终结果可能是:

  • T1 插入 A,接着 T2 插入 B(或反之)
  • 导致链表中只有一个 k1 对应的节点,另一个被覆盖

🟡 本质原因:两个线程并发执行 put 操作,没有加锁,导致链表结构不一致,后写的覆盖了先写的。


三、丢键值对是怎么发生的?

场景:

两个线程 T1T2,都向不同桶中写入数据:

  • T1 向桶 A 写入 key1
  • T2 向桶 B 写入 key2

但由于 HashMap 的底层数组是共享的,并且扩容和插入都依赖非原子操作,因此会发生以下竞态问题:

可能情况:

  • T1 将 key1 插入桶 A,但在执行过程中,T2 正在扩容并拷贝旧数据到新数组。
  • T1 完成插入后,但写入的是旧数组。
  • T2 扩容完成并用新数组替换旧数组,此时旧数组中的 key1 没有被迁移过去。

🟥 最终结果:key1 被丢失,HashMap 中找不到 key1。


四、JDK 1.7 的死循环问题

在 JDK 1.7 中,HashMap 的扩容采用的是链表头插法,当多个线程并发触发 resize(),可能发生链表环形连接,导致死循环:

while (e != null) {
    Entry<K,V> next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
}

上述代码在无同步控制下可能导致某节点的 next 指向自身或前面节点,形成 环形链表,遍历时永远无法终止,CPU 飙升。


五、总结:为何线程不安全?

操作 问题描述
put() 并发 导致覆盖写、数据丢失
resize() 并发 导致链表错乱、死循环
缺乏同步机制 没有加锁、没有原子操作保护

六、如何解决?

  • ✅ 使用 ConcurrentHashMap(JDK 8 后基于 CAS + synchronized,线程安全)
  • ✅ 用 Collections.synchronizedMap() 包装 HashMap
  • ❌ 不建议在多线程中直接使用 HashMap

七、面试总结话术

HashMap 是线程不安全的,原因包括:put 操作非原子,可能出现覆盖写或数据丢失;在并发扩容时可能出现链表死循环(JDK 1.7)。这些问题的根本原因是缺乏同步机制。在并发场景下建议使用 ConcurrentHashMap 替代。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容