注本文是笔者在学习【极客时间】业务开发常见错误过程中整理记载的个人学习和思考笔记在高并发编程中ConcurrentHashMap 因线程安全特性被广泛使用但很多开发者误以为「用了 ConcurrentHashMap 就万事大吉」却忽略了复合操作的原子性问题。本文通过一个真实的代码案例拆解 ConcurrentHashMap 的常见误用场景并给出正确的解决方案。一、案例场景并发填充 ConcurrentHashMap先看这段 Spring Boot 控制器代码核心逻辑是初始化一个包含 900 个元素的 ConcurrentHashMap然后启动 10 个线程并发补充元素目标是让最终元素总数达到 1000。1. 问题代码wrong 接口GetMapping(wrong/concurrenthashmap)publicStringwrong()throwsInterruptedException{// 初始化900个元素ConcurrentHashMapString,LongconcurrentHashMapgetData(ITEM_COUNT-100);log.info(init size:{},concurrentHashMap.size());ForkJoinPoolforkJoinPoolnewForkJoinPool(THREAD_COUNT);// 10个线程并发补充元素forkJoinPool.execute(()-IntStream.rangeClosed(1,10).parallel().forEach(i-{// 致命问题计算缺口 补充元素 非原子操作intgapITEM_COUNT-concurrentHashMap.size();log.info(线程{} - gap size:{},Thread.currentThread().getName(),gap);concurrentHashMap.putAll(getData(gap));}));forkJoinPool.shutdown();forkJoinPool.awaitTermination(1,TimeUnit.HOURS);log.info(finish size:{},concurrentHashMap.size());// 结果远大于1000returnOK;}2. 运行结果预期最终 size1000但实际运行结果往往是 1500、1800 甚至更高完全超出预期。二、问题根源复合操作缺乏原子性ConcurrentHashMap 仅保证单个方法如 put、get、size的线程安全但多个方法组合的复合操作不具备原子性。这个案例中核心问题出在两步操作1. 非原子的「计算缺口 补充元素」// 步骤1计算需要补充的元素数量intgapITEM_COUNT-concurrentHashMap.size();// 步骤2补充gap个元素concurrentHashMap.putAll(getData(gap));线程 A 执行gap 1000 - 900 100准备补充 100 个元素线程 B 同时执行gap 1000 - 900 100也准备补充 100 个元素线程 A 先完成 putAllMap 大小变为 1000线程 B 仍按之前计算的 gap100 执行 putAll最终 Map 大小变为 110010 个线程重复此过程最终 size 远大于 1000。2. 对 ConcurrentHashMap 的认知误区很多开发者误以为「ConcurrentHashMap 是线程安全的所以所有操作都安全」但事实是ConcurrentHashMap 仅保证单个方法调用的原子性如 put 时不会出现数据覆盖跨方法的复合操作如「读 size → 计算 → 写数据」必须手动保证原子性。三、正确解决方案correct 接口核心思路给「计算缺口 补充元素」的复合操作加锁确保同一时间只有一个线程执行该逻辑。GetMapping(correct/concurrenthashmap)publicStringcorrect()throwsInterruptedException{ConcurrentHashMapString,LongconcurrentHashMapgetData(ITEM_COUNT-100);log.info(init size:{},concurrentHashMap.size());ForkJoinPoolforkJoinPoolnewForkJoinPool(THREAD_COUNT);forkJoinPool.execute(()-IntStream.rangeClosed(1,10).parallel().forEach(i-{// 加锁保证复合操作的原子性synchronized(concurrentHashMap){intgapITEM_COUNT-concurrentHashMap.size();log.info(线程{} - gap size:{},Thread.currentThread().getName(),gap);// 增加边界判断避免gap为负数时添加空数据if(gap0){concurrentHashMap.putAll(getData(gap));}}}));forkJoinPool.shutdown();forkJoinPool.awaitTermination(1,TimeUnit.HOURS);log.info(finish size:{},concurrentHashMap.size());// 稳定为1000returnOK;}关键优化点加锁范围精准仅对「计算 gap putAll」的复合操作加锁避免锁范围过大影响性能增加边界判断gap ≤ 0 时不再执行 putAll避免无效操作锁对象选择直接使用 ConcurrentHashMap 实例作为锁对象也可自定义专用锁。四、ConcurrentHashMap 避坑指南1. 核心原则区分「单个操作」和「复合操作」操作类型是否线程安全示例单个方法调用安全map.put(k, v)、map.get(k)复合操作不安全先 get 再 put、先 size 再 put2. 常见误用场景 解决方案误用场景错误原因正确方案先判断 key 是否存在再 put 数据判断和 put 非原子操作使用putIfAbsent(k, v)方法先读 size再根据 size 写数据读和写非原子操作加锁synchronized/Lock循环遍历 Map 同时修改元素遍历和修改竞态条件使用迭代器的原子操作或加锁遍历多个 put 操作需保证整体成功单个 put 安全但整体不安全加锁或使用事务如数据库兜底3. 性能优化建议加锁时最小化锁范围仅锁定复合操作的核心逻辑避免整个方法加锁优先使用 ConcurrentHashMap 提供的原子方法如putIfAbsent、compute、merge等减少手动加锁高并发场景下可考虑分段锁/分片处理降低锁竞争。4. 替代方案按需选择如果业务允许最终一致性可使用 AtomicLong 统计数量单独维护 MapJDK 1.8 可使用LongAdder替代 AtomicLong 统计计数性能更高极端高并发场景可考虑使用 Disruptor 等无锁框架。五、总结ConcurrentHashMap 是线程安全的但它保护的是「单个方法」而非「业务逻辑」。使用时必须牢记复合操作必须手动保证原子性加锁或使用内置原子方法避免认知误区线程安全容器 ≠ 线程安全业务逻辑锁范围越小越好在保证线程安全的前提下尽可能减少锁竞争。希望本文能帮你避开 ConcurrentHashMap 的常见陷阱写出更健壮的高并发代码