← 每日面试题 学习看板

HashMap用在并发场景中有什么问题?

面试题/00java/01Java 基础/05 集合/HashMap用在并发场景中有什么问题?.md

面试记录 2026-10-09

题目:HashMap 用在并发场景中有什么问题?

难度:medium

标签:java/collections, general/knowledge

评分:⭐⭐⭐☆☆(3/5)

我的回答

HashMap 在并发场景中,由于它没有做锁的操作,多个线程同时进行操作的时候,会导致插入的是插入同一位置的对象出现冲突。在 JDK 1.8 之前,有比较严重的问题,就是它原本是用头插法,如果前一个头断开了,然后再重新指向的时候指向了自己,就会导致整个链路锁死。JDK 1.8 之后修复了这个问题,改成了尾插法,但是并不能弥补并发下插入内容插错的问题。多线程下,size 是同时插入,size 增加由于不是原子性的,也没有锁,会导致 size 错误,然后插入的 put 的内容被覆盖丢失掉。处理这个问题,核心其实在加锁。对应的主流的、主要的还是使用 ConcurrentHashMap 去进行线程安全的 HashMap 处理,或者是使用对应的那个工具类中 ConcurrentNavigableMap 方法来创建一个线程安全的线程安全的 HashMap。

点评

优点:

不足:

补充(JDK8 HashMap 并发问题全景):

替代方案对比:

工程实践:优先 ConcurrentHashMap;复合操作用 computeIfAbsent;不要用 HashMap 存共享统计计数(用 LongAdder);不可变场景用 Collections.unmodifiableMap

完整讲解

第一部分:知识重点剖析

核心考点定位:验证对 HashMap 并发缺陷的全面认识,尤其是 JDK8 尾插法修复的边界——它只修了一个 bug,不代表线程安全。

知识点拆解:

  1. 底层原理:HashMap 的所有操作(put/get/resize)都依赖非原子读-改-写,无内存可见性保证。JDK7 头插法在并发扩容时会翻转链表,形成 e→f→e 环形链表导致 CPU 100%。JDK8 改尾插法修复了这个特定 bug,但其他并发问题依然存在。
  2. JDK8 遗留的并发问题:
  1. 设计权衡:HashMap 追求单线程性能,把所有并发正确性成本转嫁给调用者。选型时应根据"是否并发"直接决定用哪个类,而非在运行时加锁补救。
  2. 工程实践:默认用 ConcurrentHashMap;低并发场景用 Collections.synchronizedMap;复合操作用 computeIfAbsent/merge;不可变场景用 unmodifiableMap;共享计数器用 LongAdder/AtomicLong。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. JDK8 尾插法为什么不能完全修复并发问题?
  1. computeIfAbsent 在 ConcurrentHashMap 上为什么能保证原子?
  1. Collections.synchronizedMap 的迭代要外层加锁吗?为什么?
  1. 为什么共享计数器推荐 LongAdder 而不是 AtomicLong?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,HashMap 在并发场景下有多个层面的问题。

第一个是 JDK7 的经典问题——头插法导致的环形链表。并发扩容时两个线程同时搬运链表,A 遍历到 e→f,B 遍历到 f→e,A 完成后变成 e→f→e 环,任何 get 命中这个桶都会死循环、CPU 100%。JDK8 改成了尾插法,这个问题修复了,但只是修复了这一个 bug,不代表 HashMap 就线程安全了。

第二个是 size 计数不准。++size 是 read-modify-write 的复合操作,两个线程同时 put 时可能都读到同一个 size 值,加 1 后写入,导致实际 count 少 1。

第三个是 put 覆盖丢失。HashMap 的 put 逻辑是"先检查 key 是否存在,存在则覆盖 value,不存在则插入",这个 check-then-act 中间没有锁保护。两个线程同时 put 不同 key 到同一个桶,其中一个可能会被另一个的写入覆盖。

第四个是 get 读到中间状态。并发扩容时,节点可能已经从旧桶摘除但还没挂到新桶,此时 get 命中该桶会返回 null。

第五个是迭代器的问题。HashMap 是 fail-fast 的,遍历过程中被修改会抛 ConcurrentModificationException。

第六个是内存可见性。HashMap 内部变量都不是 volatile,线程 A 修改后线程 B 可能读不到最新值。

解决方案上,主流就是用 ConcurrentHashMap——JDK8 用桶级 synchronized + CAS,读几乎无锁,并发写入也只锁一个桶。低并发简单场景可以用 Collections.synchronizedMap(new HashMap<>()) 包一层全表锁,但要注意迭代时仍需外层加锁。需要有序的场景用 ConcurrentSkipListMap 代替 TreeMap。复合操作(如"不存在则创建")直接用 computeIfAbsent 或 merge,它们在 ConcurrentHashMap 上能保证原子。共享计数器不要用 Map 存 AtomicLong,直接用 LongAdder 分片累加性能更好。