HashMap 是 Java 里使用最广泛的集合之一,但它不是线程安全的。在多线程环境下直接使用,轻则数据错乱,重则引发 CPU 100% 的系统故障。这篇文章讲清楚隐患在哪,以及正确的替代方案。

一、HashMap 有哪些并发隐患

1. 数据丢失与脏读

多个线程同时 put 时,可能出现覆盖写、丢失更新。例如两个线程同时向同一个桶写入,后写的可能覆盖前写的,导致数据不完整。

2. 扩容时形成环形链表(老版本)

JDK 7 及之前,HashMap 的 rehash 在并发插入触发扩容时可能让链表的 next 指针形成环。一旦发生,后续 get 会陷入死循环,CPU 飙到 100%。JDK 8 之后改用尾插法和红黑树优化,环形链表问题被改善,但并发下的数据一致性依然没有保证。

二、并发场景的替代方案

1. ConcurrentHashMap(首选)

JDK 5 引入,JDK 8 后采用 CAS + synchronized 对单个桶加锁,读写性能好,且不需要额外加锁:

Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
Integer v = map.get("key");

2. Collections.synchronizedMap

对整个方法加同步锁,简单但并发度低,适合访问量小的场景:

Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());

三、并发小技巧:原子操作

即使是 ConcurrentHashMap,组合操作仍然需要小心。比如「不存在才放入」这种逻辑,不要写成下面这样:

// 不推荐:判断和写入是两个原子操作之间的空档
if (!map.containsKey(key)) {
    map.put(key, value);
}

推荐使用线程安全的原子方法:

map.putIfAbsent(key, value);          // 不存在才放
map.computeIfAbsent(key, k -> value); // 按需初始化

四、小结

  • 单线程用 HashMap;多线程写/读用 ConcurrentHashMap。
  • 别用 Hashtable 替代——它全表加锁,性能差。
  • 多线程下避免「先查后写」的组合操作,优先使用 computeIfAbsent、putIfAbsent 等原子方法。