在Java里HashMap为何线程不安全_Java并发问题说明

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

它只保证单个操作(putgetremove)的线程安全,不提供复合操作的原子性。比如:

  • if (!map.contains

    Key(key)) map.put(key, value);
    —— 存在竞态窗口,应改用 map.putIfAbsent(key, value)
  • map.get(key) + 1 后再 put —— 不是原子的,需用 compute()merge()
  • 迭代期间有写入,可能抛 ConcurrentModificationException(虽然概率比 HashMap 小,但依然存在)

真正需要“读多写少+强一致性”的场景,还得考虑 CopyOnWriteArrayList 或显式加锁,别默认以为换了 ConcurrentHashMap 就万事大吉。