ConcurrentHashMap仅保证单个操作原子性,不保证复合操作一致性;需用putIfAbsent、compute或merge替代if-put等非原子逻辑,遍历时应避免强一致性要求,mappingCount()比size()更精确。

ConcurrentHashMap 的 get/put 为什么不能直接保证业务一致性
很多人以为只要用了 ConcurrentHashMap,多线程写入就不会丢数据、读取就一定能看到最新值——这是错觉。它只保证**单个操作的原子性**(如 put、get、remove),不保证**复合操作的原子性**。
比如「先检查 key 是否存在,再 put」这种逻辑:if (!map.containsKey("k")) map.put("k", v);,在高并发下仍可能重复插入,因为 containsKey 和 put 是两个独立调用,中间可能被其他线程插入同 key。
-
putIfAbsent("k", v)才是等价的原子操作,推荐替代手写 if-put -
computeIfAbsent("k", k -> expensiveInit())适合延迟初始化且避免重复计算 - 涉及计数类场景(如累加),别用
get + put,改用merge或compute系列方法
什么时候必须用 computeXXX 而不是 get + 修改 + put
典型错误模式:Integer old = map.get("cnt"); map.put("cnt", old == null ? 1 : old + 1);。这会丢失更新:两个线程同时读到 old = 5,都算出 6,最终结果是 6 而非期望的 7。
compute 系列方法底层基于 CAS 或锁重试,确保整个读-改-写过程不可打断:
-
map.compute("cnt", (k, v) -> v == null ? 1 : v + 1)—— 安全递增 -
map.merge("cnt", 1, Integer::sum)—— 更简洁的累加写法 - 注意:lambda 体内不要做耗时或阻塞操作,否则会拖慢整个桶的写入性能
遍历时修改导致的 ConcurrentModificationException 怎么避坑
ConcurrentHashMap 的迭代器是**弱一致性**的:遍历时允许其他线程修改,但不会抛 ConcurrentModificationException。可这不意味着安全——你可能读到过期值、漏掉新 entry,甚至看到部分更新的中间状态。
- 如果业务要求「遍历期间数据静止」,别依赖迭代器,改用
mappingCount()+keySet().toArray()快照,再逐个处理 -
forEach和replaceAll是安全的,它们内部使用分段遍历+重试机制,但仅适用于纯函数式逻辑(无副作用) - 绝对不要在
forEachlambda 里调用map.remove(k)—— 这属于未定义行为,JDK 不保证结果
size() 和 mappingCount() 的区别和误用风险
size() 在 JDK 8+ 中是**估算值**:它通过遍历所有 segment / bin 统计,但过程中其他线程可能正在增删,所以返回值可能滞后于真实数量。而 mappingCount() 返回的是 long 类型的精确计数(基于 volatile long baseCount + counterCells 数组的 CAS 累加)。
- 做容量判断(如「如果 size > 1000 就清理」)必须用
mappingCount(),否则条件可能永远不触发 -
size()主要为兼容老代码保留,新项目应默认忽略它 - 即使用了
mappingCount(),它也只反映调用瞬间的映射数,无法替代分布式锁或数据库事务来保证跨操作一致性
真正难的从来不是选对容器,而是厘清「一致性」到底指哪一层:是单 key 原子写?是某次批量操作的可见性?还是跨多个 key 的业务约束?ConcurrentHashMap 只解决前者;后两者得靠设计权衡,比如拆 key、引入版本号、或者该上数据库的时候别硬扛。

















