hashmap多线程put必然导致数据丢失或结构损坏,因resize时多线程覆盖新数组、链表转树并发异常、get依赖被破坏结构而返回null或死循环;concurrenthashmap通过分段锁(jdk7)或cas+synchronized单桶锁(jdk8)及volatile字段保障安全。

HashMap put 时的多线程竞态会导致数据丢失
多个线程同时执行 put,可能在扩容判断、节点插入、链表转红黑树等环节发生覆盖或跳过。典型现象是:明明 put 了 1000 个键值对,最终 size() 小于 1000,甚至为 0。
-
transient Node<k>[] table</k>是共享的,但 resize() 过程中多个线程都可能新建新数组并赋值给table,后执行的线程会直接覆盖前者的成果 - 当两个线程同时发现需要扩容(
size > threshold),都调用resize(),各自生成新数组,最终只有一个生效,另一个中已 rehash 的节点彻底丢失 - 在 JDK 7 中还存在“头插法”导致的环形链表问题,JDK 8 改为尾插但仍无法避免并发修改结构引发的
ConcurrentModificationException或静默错误
get 操作在并发写入时可能无限循环或返回 null
这不是因为 get 本身加锁,而是它依赖的底层结构被其他线程正在破坏。最典型的是 JDK 7 中的环形链表:一个线程正在扩容重哈希并用头插法迁移节点,另一个线程恰好执行 get,遍历链表时陷入死循环。
- JDK 8 虽改用尾插法避免成环,但若在
putTreeVal(向红黑树插入)和treeifyBin(链表转树)之间发生并发,可能导致树节点未完全初始化就被读取,get返回null或抛出NullPointerException - 即使没崩溃,
get也可能读到过期的table引用(因缺乏 volatile 语义保障),查不到刚由其他线程写入的键
ConcurrentHashMap 是怎么解决的
不是靠全局锁,而是分段 + CAS + volatile + synchronized 局部化。
- JDK 7 使用
Segment[]分段锁,每段独立加锁;JDK 8 彻底移除 Segment,改用Node[]数组 +synchronized锁单个桶(即链表头节点或红黑树根节点) - 关键字段如
sizeCtl和nextTable均为volatile,保证扩容状态可见;tabAt/casTabAt等 Unsafe 操作确保数组元素更新的原子性 - 扩容时允许多线程协助迁移(
helpTransfer),且每个线程只负责自己分配的桶范围,不互相阻塞
别拿 Collections.synchronizedMap 化解面试陷阱
它只是对所有 public 方法加了 synchronized(this),看似线程安全,实则掩盖了复合操作的漏洞。
立即学习“Java免费学习笔记(深入)”;
-
if (!map.containsKey(key)) map.put(key, value);在 synchronizedMap 中仍是非原子的:两次方法调用之间可能被其他线程插入相同 key - 迭代器仍会抛
ConcurrentModificationException,因为内部 modCount 检查未同步保护 - 性能比 ConcurrentHashMap 差很多,尤其高并发读场景下,读操作也被串行化
Map<String, Integer> unsafe = new HashMap<>(); Map<String, Integer> safe = new ConcurrentHashMap<>(); // 正确选择 // Map<String, Integer> wrong = Collections.synchronizedMap(new HashMap<>()); // 面试时慎提
真正要注意的是:HashMap 的线程不安全不是“偶尔出错”,而是“只要并发写就必然不可预测”。哪怕只有一处 put 和一处 get 同时发生,也可能触发底层结构损坏——这正是面试官想确认你是否理解其底层实现而非背结论的地方。










