JDK 7 HashMap 多线程扩容时因头插法易形成环形链表导致死循环;JDK 8 改用尾插法消除该问题,但仍未解决覆盖、size失真、可见性等线程安全问题;ConcurrentHashMap 通过分段锁(JDK 7)或CAS+桶级锁(JDK 8+)及volatile保证安全,适用于高并发读写;而需强一致性操作(如复合判断+插入、遍历中修改)时应选Collections.synchronizedMap。

HashMap在多线程下会触发resize导致死循环(JDK 7)
JDK 7 中 HashMap 的 resize() 操作使用头插法迁移链表节点,多线程同时扩容时可能形成环形链表。一旦后续调用 get() 或 put() 触发遍历,就会陷入无限循环,CPU 占用飙到 100%。
这不是偶发 bug,而是确定性并发缺陷。只要两个线程几乎同时触发扩容,且操作的是同一个桶(bucket),就大概率复现。
- 仅存在于 JDK 7 及更早版本;JDK 8 改为尾插法,消除了环形链表,但仍有其他线程安全问题
- 不需要高并发压测,简单写个双线程反复
put()就能复现 - 现象是程序卡死、无异常、无日志,排查难度大
Mapmap = new HashMap<>(); // 线程1 和 线程2 同时执行以下逻辑 for (int i = 0; i < 10000; i++) { map.put("key" + i, "value"); }
即使JDK 8也存在put覆盖、size失真、数据丢失等问题
JDK 8 虽修复了死循环,但 HashMap 本身仍没有任何同步机制。put()、get()、size() 等方法都非原子操作,多线程下行为不可预测。
-
put()可能覆盖:线程A读取旧值→线程B写入新值→线程A基于旧值计算后写入,覆盖B的结果 -
size()返回值不准确:它只是返回内部modCount和元素计数的快照,不是实时统计 - 迭代器抛
ConcurrentModificationException:不是因为“安全”,而是检测到结构被修改后的快速失败机制 - 没有内存可见性保证:一个线程写入的 key-value,另一个线程可能永远看不到(缺少 volatile 或锁)
ConcurrentHashMap为什么能解决这些问题
ConcurrentHashMap 不是简单加 synchronized,而是通过分段锁(JDK 7)或 CAS + synchronized 锁单个桶(JDK 8+)实现更高并发度。
立即学习“Java免费学习笔记(深入)”;
- JDK 8 中,
put()先尝试无锁 CAS 插入;失败再对对应桶(table[i])加synchronized锁,不影响其他桶操作 -
size()通过baseCount+ 多个CounterCell分散更新,避免竞争,再聚合计算 - 所有读操作(
get())完全无锁,依赖volatile语义和数组引用的不可变性保证可见性 - 迭代过程允许弱一致性:不抛 CME,可容忍部分更新未被看到,但不会崩溃或死锁
Mapmap = new ConcurrentHashMap<>(); // 安全 map.put("a", "1"); // 并发调用没问题 String v = map.get("a"); // 不加锁也能安全读
什么时候不该用ConcurrentHashMap而该用Collections.synchronizedMap
不是所有场景都适合 ConcurrentHashMap。它牺牲了强一致性来换取性能,如果业务需要「一次操作覆盖多个键」或「遍历时必须看到全部当前状态」,反而要用 Collections.synchronizedMap()。
- 需要原子性复合操作:比如“检查 key 是否存在,不存在才 put”,
ConcurrentHashMap的computeIfAbsent()可以替代,但写法要改;若用if (!map.containsKey(k)) map.put(k, v),无论哪种 map 都不安全 - 遍历 + 修改需强一致:例如边遍历边删除满足条件的 entry,
ConcurrentHashMap的迭代器不支持remove(),且遍历结果可能不含最新插入项 - 吞吐量不高但要求逻辑简单:
synchronizedMap更易理解,锁粒度粗但够用,且支持Map接口全部默认方法
真正容易被忽略的点是:线程安全 ≠ 所有操作组合都安全。哪怕用了 ConcurrentHashMap,没用对它的原子方法(如 putIfAbsent、compute),照样出错。










