Java集合框架中的ConcurrentHashMap多线程安全

ConcurrentHashMap 并非完全线程安全,单个方法调用(如 put、get、remove、computeIfAbsent、merge)原子,但复合操作需额外同步;size() 返回近似值,keySet().iterator() 为弱一致性迭代器。

ConcurrentHashMap 真的完全线程安全吗?

它不是“所有操作都加全局锁”的线程安全,而是通过分段锁(JDK 7)或 CAS + synchronized(JDK 8+)实现**部分操作的线程安全**。关键在于:单个方法调用是原子的,但复合操作(如 if (map.get(k) == null) map.put(k, v))仍需额外同步。

哪些操作是原子的?哪些不是?

以下方法在 JDK 8+ 中保证单次调用的原子性:putgetremovecomputeIfAbsentmerge。但这些不是:

  • size() 返回的是近似值(可能滞后),高并发下不建议用于条件判断
  • keySet().iterator() 返回的迭代器是弱一致性(weakly consistent),可遍历到已删除元素,也可能跳过刚插入元素
  • for (E

    ntry e : map.entrySet())
    同样基于弱一致性迭代器,不保证看到全部实时变更

为什么不能用 ConcurrentHashMap 替代 synchronized 块?

常见误用是把 ConcurrentHashMap 当作“免同步万能容器”。例如:

if (!map.containsKey(key)) {
    map.put(key, expensiveCompute());
}

这仍是竞态条件:两个线程同时通过 containsKey 检查,都会执行 expensiveCompute() 并写入。正确写法是:

map.computeIfAbsent(key, k -> expensiveCompute());

该方法内部用 CAS 或细粒度锁确保只计算一次。其他类似场景也应优先使用 computeIfPresentmerge 等原子更新方法。

JDK 7 和 JDK 8+ 的底层差异影响什么?

升级后行为变化容易被忽略:

  • JDK 7 使用 Segment 数组分段加锁,concurrencyLevel 影响初始化桶数,但实际并发度由哈希分布决定
  • JDK 8+ 废除 Segment,改用 Node 数组 + 链表/红黑树,锁粒度细化到单个 Node(即数组槽位)
  • put 在 JDK 8+ 中对空槽位用 CAS,非空时再加 synchronized 锁住首节点;这意味着高冲突哈希可能导致局部锁争用
  • size() 在 JDK 8+ 中通过累加每个桶的修改计数(baseCount + sum of counterCells)估算,但不保证强一致
多线程下真正麻烦的从来不是单个 put,而是你写的那几行看似“顺手”的 if-else 逻辑。