HashMap并发put会导致数据丢失、死循环或部分初始化对象;ConcurrentHashMap通过CAS+synchronized单桶锁及协作扩容解决,但不保证复合操作原子性。

HashMap put 过程中发生扩容时的竞态条件
当多个线程同时触发 resize(),可能造成链表环形结构。JDK 7 中的头插法会让迁移后的节点顺序反转,若线程 A 搬到一半被挂起,线程 B 完成整个扩容,之后 A 继续执行,就可能把某个节点 next 指向自己——后续遍历 get() 或 size() 会陷入死循环。
put 操作不是原子的,存在中间状态暴露
put(K,V) 实际分三步:计算 hash → 定位桶 → 插入或覆盖。两个线程算出同一 hash 值、落在同一个桶,可能先后写入,后者覆盖前者,且无任何同步机制捕获丢失更新。
- 典型现象:
map.put("key", "v1")和map.put("key", "v2")并发执行后,值可能是"v1"或"v2",但调用方无法预期结果 - 更隐蔽的问题是:如果插入的是自定义对象,而该对象字段在构造中未正确发布(如未用
final),即使 put 成功,其他线程读到的也可能是部分初始化的对象
ConcurrentHashMap 为何能解决(以 JDK 8 为例)
JDK 8 的 ConcurrentHashMap 放弃了分段锁(Segment),改用 CAS + synchronized 锁单个桶(Node)的方式:
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node(hash, key, value, null)))
break;
} - 桶为空时,用
casTabAt()原子写入新节点,失败则重试 - 桶非空时,只对首节点加
synchronized,避免锁整个 table - 扩容由多个线程协作完成,每个线程负责一段区间,并通过
transferIndex协调分工
什么情况下仍不能直接用 ConcurrentHashMap
它只保证单个操作(put、get、remove)的线程安全,不提供复合操作的原子性。比如:
立即学习“Java免费学习笔记(深入)”;
-
if (!map.containsKey(key)) map.put(key, value);—— 存在竞态窗口,应改用map.putIfAbsent(key, value) -
map.get(key) + 1后再put—— 不是原子的,需用compute()或merge() - 迭代期间有写入,可能抛
ConcurrentModificationException(虽然概率比 HashMap 小,但依然存在)
真正需要“读多写少+强一致性”的场景,还得考虑 CopyOnWriteArrayList 或显式加锁,别默认以为换了 ConcurrentHashMap 就万事大吉。










