☰
Java进阶篇之ConcurrentHashMap:把共享数据的更新放进原子操作
2026/9/27 6:03:48 网站建设 项目流程

上一篇用BlockingQueue把任务从生产者交给消费者。如果消费者需要统计每个接口处理了多少次请求,就会遇到另一个问题:多个线程同时修改同一张Map,怎样才能保住每一次更新?

ConcurrentHashMap提供了并发访问能力。不过,把HashMap换成它之后,业务中的“读取、计算、写回”仍需要认真设计。这篇从计数场景出发,梳理原子方法、可变对象和统计结果之间的关系。

本文基于Java 21,文档核对日期为2026年9月26日。示例只使用JDK标准库。

一、容器安全与业务操作的边界

先看一段容易出现的写法:

Integerold=counts.getOrDefault("/orders",0);counts.put("/orders",old+1);

假设初始值为7,两个线程都读取到7,再分别写入8。最终只增加了一次。这里get和put各自能够安全执行,但两次调用之间,其他线程仍然可以进入。

可以把业务要求写成一句话:针对同一个key,将旧值加一并保存,作为一次完整更新完成。这时适合用merge:

counts.merge("/orders",1,Integer::sum);

key还没有映射时,存入1;已经存在时,合并旧值和本次增量。不要把业务条件散落在多个独立调用中,再期待容器自动把它们合成事务。

二、先认识这张并发Map

ConcurrentHashMap实现了ConcurrentMap接口。日常使用可以分成三个层次:读取映射、原子更新单个映射、观察整个容器。

层次常见方法需要明确的边界
读取get、containsKey读到的是一次访问的结果
更新单个keyputIfAbsent、compute、merge适合把同一映射的判断与更新集中起来
遍历与统计forEach、size并发修改期间不代表整张Map的固定快照

它不接受null键或null值;并发遍历采用弱一致性语义。遍历可以与更新同时进行,不会因为并发修改直接抛出ConcurrentModificationException,但也不能把结果当成同一时刻的完整快照。具体契约见Java 21的ConcurrentHashMap文档。

本文聚焦公开API。老资料中常见的Segment分段锁讲法带有实现版本背景,阅读源码时应先确认JDK版本,避免把某一版实现结构当成接口保证。

三、把一次业务更新交给一个方法

图中的两张“+1”任务卡面向同一个计数器。每张卡都完成一次合并,计数从7走到8,再走到9。图示表达的是同一个key的更新语义,没有把Map画成一把锁住所有键的大锁。

几个常用方法可以这样选:

需求方法例子
没有映射时放入默认对象putIfAbsent注册一个已准备好的配置
需要时才构造对象computeIfAbsent为某个分组创建计数器
根据旧值重新计算compute依据当前数量决定新数量
将增量合并到旧值merge累加接口调用次数
值仍符合预期才替换replace(key, old, new)尝试推进单个状态值

例如用compute表达有限库存扣减时,可以在回调里统一检查旧值和计算新值。但“扣库存后写订单、再更新另一个key”已经超出一个映射的范围,应交给合适的事务或整体同步机制设计。

还要留意回调返回null的含义:在compute或已有映射的merge重映射中,null可以表示删除映射。对于计数示例,回调明确返回整数,避免无意触发删除语义。

四、完整示例:并发累计接口调用次数

下面启动4个工作线程,每个线程对同一路径累计1000次。主线程等待所有任务结束,再读取最终结果。

importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;publicclassConcurrentMapDemo{publicstaticvoidmain(String[]args)throwsException{ConcurrentHashMap<String,Integer>counts=newConcurrentHashMap<>();CountDownLatchstart=newCountDownLatch(1);List<Future<?>>tasks=newArrayList<>();try(ExecutorServicepool=Executors.newFixedThreadPool(4)){for(intworker=0;worker<4;worker++){tasks.add(pool.submit(()->{try{start.await();for(inti=0;i<1000;i++){counts.merge("/orders",1,Integer::sum);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewIllegalStateException(e);}}));}start.countDown();for(Future<?>task:tasks){task.get();}}System.out.println("/orders="+counts.get("/orders"));System.out.println("keys="+counts.size());}}

保存为ConcurrentMapDemo.java,使用JDK 21执行:

javac-encodingUTF-8 ConcurrentMapDemo.javajavaConcurrentMapDemo

输出:

/orders=4000 keys=1

CountDownLatch只负责统一放行;Future.get负责等待任务结束并把任务异常交回主线程;准确累加来自merge所表达的原子更新。三个机制各有职责。

这里的size是在全部更新完成后读取的,因此可以验证key数量。若更新仍在持续,拿size作为是否允许下一次写入的严格判断条件,就会引入新的竞争窗口。

五、value里的对象也需要保护

考虑下面的代码:

groups.computeIfAbsent("java",key->newArrayList<>()).add("article");

即使groups本身是ConcurrentHashMap,多个线程仍可能拿到同一个ArrayList并同时add。安全保存对象引用,不等于对象内部的所有操作都具备并发安全性。

可以根据访问模式使用并发集合、为对象提供同步方法,或者以不可变值替换旧值。不要只检查容器类型,还要沿着value继续检查共享状态。

对于高频统计,官方文档也给出了LongAdder配合computeIfAbsent的用法:

frequencies.computeIfAbsent(path,key->newLongAdder()).increment();

这适合频繁累加、阶段性观察的统计业务。LongAdder.sum不提供并发更新期间的原子快照;在没有并发更新时,返回的合计值才是准确的。如果业务要求精确地基于当前值作出一次扣减决定,应重新选择同步方案,不能直接套用统计计数器。详见LongAdder.sum的官方说明。

六、几个值得提前避开的坑

回调保持短小。compute或merge回调适合局部计算,不宜塞入远程调用、长时间等待或复杂的嵌套更新。耗时操作会扩大竞争影响,异常处理也更难解释。

多key约束单独设计。“A减少一份、B增加一份”需要整体保持约束。两次单key更新即使分别安全,中间状态仍可能被观察到。

可变key谨慎使用。作为键参与equals和hashCode的内容应保持稳定。若放入Map后改变这些字段,后续查找可能无法按原预期定位。

先用契约写正确,再通过测量优化。本文没有比较不同实现的吞吐量。容量、key分布、读写比例和回调开销都会影响表现,实际压测要贴近业务负载。

七、🧠 思维导图

ConcurrentHashMap

并发访问

不接受null

遍历弱一致性

单key原子更新

merge合并增量

compute重新计算

computeIfAbsent初始化

业务边界

多次调用存在间隙

value仍需并发设计

多key约束单独保护

使用建议

回调短小

结束后核对最终统计

八、总结

总结要点

原子更新应围绕具体业务动作组织。一次计数尽量写成一次merge,避免把读取和写回分散成两个可被插入的步骤。

共享对象需要逐层检查。ConcurrentHashMap保护映射关系,value对象的修改、多key之间的约束以及跨系统操作仍需要各自的并发方案。

统计与判断有不同要求。运行中的遍历适合观察,严格决策需要明确一致性边界;示例在任务全部结束后验证最终值,结论也限定在这个场景内。

后续预告:下一篇继续Java进阶系列,聊聊ReentrantLock,看看显式锁如何配合tryLock、可中断等待和finally管理临界区。

👉如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询