深蓝黑背景品牌封面:左上大标题「HashMap 为什么线程不安全」,副标题「并发场景该用什么」;底部三块对照牌——HashMap(并发写入会互相覆盖)、Hashtable(全表一把锁,慢)、ConcurrentHashMap(桶级锁加 CAS,推荐);顶部标注「经典面试题解析 · Java 后端」与「Day 02 · 09-20」 图 1:本文封面,点明题目与解题方向。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料(见文末来源说明),与任何公司的当前招聘无关。这道题表面考一个类的用法,实际考的是你对「并发读写共享数据」这件事有没有工程直觉。

想象面试现场,面试官问:「HashMap 用过吧?它线程安全吗?为什么?并发场景该换成什么?」

第一步,审题。

这题有三层:HashMap 的内部结构是什么;并发下具体会出什么问题;替代方案各自怎么解决。只答「不安全,用 ConcurrentHashMap」这句话人人都背得出,分水岭在中间那层——说不出具体的故障现象,就等于没答。

第二步,基础回答:结构与风险点。

HashMap 的结构是「桶数组 + 桶内链表」:key 的哈希值决定落在哪个桶,哈希冲突的多个 key 在同一个桶里挂成链表。官方文档写明两个关键参数:默认初始容量 16、默认负载因子 0.75,当条目数超过「容量 × 负载因子」就触发 rehash——桶数组扩容到大约两倍,所有条目重新分布。文档还说明,当大量 key 的哈希值相同且 key 可比较时,会用比较顺序帮助打破平局(对应实现里的链表转树)。

风险点恰恰长在这些写路径上。官方文档的原话是:这个实现没有做同步,多个线程并发访问且至少一个线程做结构性修改(增删条目)时,必须从外部同步。不做同步会发生什么?两个线程同时 put,彼此的写入可能互相覆盖,数据丢失;一个线程读、另一个线程正在扩容,读到的可能是搬迁到一半的中间状态;size 这类统计值也不再可信。早期 JDK 版本的实现在并发扩容下甚至可能让链表成环,把后续查询拖进死循环——新版本改变了扩容时的搬移方式,但数据错乱的风险依旧存在,「不死循环」不等于「安全」。

文档还点了一个更隐蔽的坑:HashMap 的迭代器是 fail-fast 的,迭代过程中发现结构被改动会抛出 ConcurrentModificationException,但这只是尽力而为的检测,官方明确警告不能拿它当正确性保障——它只用于发现 bug,不用于防止 bug。

结构对比图:左侧 HashMap 是数组加链表(长链转红黑树)的桶结构,线程 A 与线程 B 同时 put 到同一个桶时互相覆盖、丢失数据;右侧 ConcurrentHashMap 把锁细化到桶——空桶用 CAS 无锁写入,桶内冲突只对桶头加 synchronized,其它桶照常读写,并发度约等于桶的数量。底部演进对照:Hashtable 全表锁 → 早期分段锁 → 现在的 CAS 加桶级锁;并提示具体实现以当前 JDK 源码为准 图 2:HashMap 的并发覆盖现场,与 ConcurrentHashMap 的桶级锁细化。

第三步,替代方案怎么选。

Hashtable 和 Collections.synchronizedMap 是同一路数:所有方法用一把全局锁包住,正确性有了,并发度没了——任何两个操作都串行。官方文档给 synchronizedMap 的定位很清楚:创建时包装、防止意外的非同步访问,适合低并发的保底场景。

ConcurrentHashMap 是另一条路线。官方文档写明它的设计目标:检索操作完全并发、更新操作保持高预期并发;get 等检索操作通常不阻塞,可以与 put、remove 交叠进行;所有操作线程安全,但不提供「锁全表阻止一切访问」的能力。实现上它把锁的粒度从整张表拆细到桶级别,读路径基本无锁,这才换来读写都快的并发表现。同时要注意文档的另一句提醒:size、isEmpty 这类聚合状态在并发更新过程中只反映瞬时值,适合监控估算,不适合拿来做程序控制逻辑。

第四步,可能的追问。

「读多写少选什么?」ConcurrentHashMap 读无锁,首选。「需要复合操作原子性怎么办?」用 ConcurrentHashMap 自带的 putIfAbsent、compute、merge 等原子方法,而不是「先 get 再 put」——后者是两步操作,中间照样会被插入。「复合操作还不够呢?」那就要在应用层自己加锁,或者重新设计,把共享状态收敛掉。

工程应用与 AI 关联。

AI 推理服务的会话缓存、特征缓存、限流计数器都是典型的高并发读写场景。容器选错不是慢的问题,是数据静默出错的问题——会话串号、计数漂移,线上事故往往就这么来。后端转 AI 工程,这道题对应的正是基本功。

易错点清单。

一,把「线程不安全」理解成「一定会报错」——它更多是静默写错数据。二,以为 fail-fast 异常是保护机制,其实它只是检测手段。三,以为 Hashtable 和 ConcurrentHashMap 差别只在名字,不讲锁粒度。四,用「get 判断 + put 写入」代替原子复合方法。

回到面试现场,建议的答题节奏:先讲结构(桶、链表、扩容),再讲并发下的具体故障(覆盖、中间态、统计失真),然后对比三种方案的锁粒度,最后落到选型原则。故障现象讲得越具体,越说明你真踩过或真想过。

如果面试官追问「ConcurrentHashMap 为什么不允许 null 的 key 和 value」,你能从并发语义的角度给出解释吗?欢迎写出你的答案。

参考资料

  • HashMap(JDK 21 官方文档:默认容量 16 与负载因子 0.75、超阈值 rehash 翻倍、实现未同步须外部同步、fail-fast 迭代器仅供检测 bug、Comparable key 比较破平局) — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html
  • ConcurrentHashMap(JDK 21 官方文档:检索全并发、更新高预期并发、检索不加锁不阻塞、所有操作线程安全、size 等聚合状态只反映瞬时值) — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ConcurrentHashMap.html