当前位置: 首页 > news >正文

【避坑指南】ConcurrentHashMap 并发计数优化实战

在高并发编程中,ConcurrentHashMap是我们处理线程安全映射的常用工具,但如果使用不当,不仅无法发挥其并发优势,反而会让代码性能跌入谷底。本文将通过一个真实的反面案例,拆解并发计数代码的核心问题,一步步优化并验证性能提升效果。

一、场景背景:多线程统计随机Key出现次数

我们的需求很简单:

  • 启动10个线程,循环1000万次
  • 随机生成10个Key(item0~item9)
  • 统计每个Key被命中的次数
  • 保证线程安全,最终计数准确

先看原始实现代码(反面教材):

importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ForkJoinPool;importjava.util.concurrent.ThreadLocalRandom;importjava.util.concurrent.TimeUnit;importjava.util.stream.IntStream;publicclassOriginalCounterWithTime{// 循环次数privatestaticintLOOP_COUNT=10000000;// 线程数量privatestaticintTHREAD_COUNT=10;// 元素数量privatestaticintITEM_COUNT=10;privateMap<String,Long>normaluse()throwsInterruptedException{// 记录方法开始时间(毫秒)longstartTime=System.currentTimeMillis();ConcurrentHashMap<String,Long>freqs=newConcurrentHashMap<>(ITEM_COUNT);ForkJoinPoolforkJoinPool=newForkJoinPool(THREAD_COUNT);forkJoinPool.execute(()->IntStream.rangeClosed(1,LOOP_COUNT).parallel().forEach(i->{// 获得一个随机的KeyStringkey="item"+ThreadLocalRandom.current().nextInt(ITEM_COUNT);synchronized(freqs){if(freqs.containsKey(key)){// Key存在则+1freqs.put(key,freqs.get(key)+1);}else{// Key不存在则初始化为1freqs.put(key,1L);}}}));forkJoinPool.shutdown();forkJoinPool.awaitTermination(1,TimeUnit.HOURS);// 计算总耗时并打印longendTime=System.currentTimeMillis();longcostTime=endTime-startTime;System.out.println("=== 原始代码执行耗时统计 ===");System.out.println("循环次数:"+LOOP_COUNT);System.out.println("线程数量:"+THREAD_COUNT);System.out.println("总耗时:"+costTime+" 毫秒("+(costTime/1000.0)+" 秒)");returnfreqs;}// 主方法:程序入口,可直接运行publicstaticvoidmain(String[]args)throwsInterruptedException{OriginalCounterWithTimecounter=newOriginalCounterWithTime();Map<String,Long>result=counter.normaluse();// 打印统计结果并校验总数(验证计数是否准确)System.out.println("\n=== 统计结果 ===");longtotal=0;for(Map.Entry<String,Long>entry:result.entrySet()){System.out.println(entry.getKey()+": "+entry.getValue());total+=entry.getValue();}System.out.println("统计总数:"+total);System.out.println("预期总数:"+LOOP_COUNT);System.out.println("计数是否准确:"+(total==LOOP_COUNT));}}

二、原始代码的核心问题剖析

1. 最大问题:完全浪费 ConcurrentHashMap 的并发优势

ConcurrentHashMap的设计核心是细粒度锁(JDK1.8后为CAS+分段synchronized),原本支持不同Key的操作并行执行。但代码中synchronized (freqs)对整个Map加了全局锁,导致所有线程无论操作哪个Key都必须排队,相当于把并发程序变成了串行执行。

形象点说:就像买了一辆跑车(ConcurrentHashMap),却用绳子绑死车轮(全局锁),跑车只能像自行车一样慢走。

2. 操作冗余且不够优雅

即使加了全局锁,containsKey() + get() + put()是三次独立的哈希查找操作,虽然锁能保证安全,但多了一次无意义的containsKey()检查,增加了性能开销。

3. 资源管理不严谨

forkJoinPool.shutdown()未放在finally块中,若执行过程中抛出异常,线程池无法关闭,会导致系统资源泄漏(线程、CPU占用)。

4. 鲁棒性差

核心逻辑无异常处理,一旦并行流内部出错,程序直接中断且无法定位问题。

三、原始代码性能测试

运行上述代码,在普通8核CPU机器上的结果:

=== 原始代码执行耗时统计 === 循环次数:10000000 线程数量:10 总耗时:1280 毫秒(1.28 秒) === 统计结果 === item0: 999876 item1: 1000123 item2: 999988 ... 统计总数:10000000 预期总数:10000000 计数是否准确:true

关键结论:计数结果准确,但10个线程的执行耗时和单线程几乎无差异(甚至略慢,因为加解锁有额外开销)。

四、代码优化方案与实现

针对上述问题,我们做核心优化:

优化核心思路

  1. 移除全局synchronized锁,使用ConcurrentHashMap内置的原子方法
  2. 简化计数逻辑,减少冗余操作
  3. 完善资源管理和异常处理

优化后的完整代码:

importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ForkJoinPool;importjava.util.concurrent.ThreadLocalRandom;importjava.util.concurrent.TimeUnit;importjava.util.stream.IntStream;/** * 优化后的并发计数代码 - 保留耗时统计,方便对比 */publicclassOptimizedCounterWithTime{// 循环次数privatestaticintLOOP_COUNT=10000000;// 线程数量privatestaticintTHREAD_COUNT=10;// 元素数量privatestaticintITEM_COUNT=10;privateMap<String,Long>optimizedUse()throwsInterruptedException{// 记录方法开始时间(毫秒)longstartTime=System.currentTimeMillis();ConcurrentHashMap<String,Long>freqs=newConcurrentHashMap<>(ITEM_COUNT);ForkJoinPoolforkJoinPool=newForkJoinPool(THREAD_COUNT);try{forkJoinPool.execute(()->IntStream.rangeClosed(1,LOOP_COUNT).parallel().forEach(i->{// 获得一个随机的KeyStringkey="item"+ThreadLocalRandom.current().nextInt(ITEM_COUNT);// 核心优化:使用ConcurrentHashMap原子方法替代全局synchronized锁// compute方法本身是线程安全的,且锁粒度仅针对单个keyfreqs.compute(key,(k,v)->v==null?1L:v+1);}));}finally{// 确保线程池一定会关闭,避免资源泄漏forkJoinPool.shutdown();forkJoinPool.awaitTermination(1,TimeUnit.HOURS);}// 计算总耗时并打印longendTime=System.currentTimeMillis();longcostTime=endTime-startTime;System.out.println("=== 优化后代码执行耗时统计 ===");System.out.println("循环次数:"+LOOP_COUNT);System.out.println("线程数量:"+THREAD_COUNT);System.out.println("总耗时:"+costTime+" 毫秒("+(costTime/1000.0)+" 秒)");returnfreqs;}// 主方法:程序入口,可直接运行publicstaticvoidmain(String[]args)throwsInterruptedException{OptimizedCounterWithTimecounter=newOptimizedCounterWithTime();Map<String,Long>result=counter.optimizedUse();// 打印统计结果并校验总数(验证计数准确性)System.out.println("\n=== 统计结果 ===");longtotal=0;for(Map.Entry<String,Long>entry:result.entrySet()){System.out.println(entry.getKey()+": "+entry.getValue());total+=entry.getValue();}System.out.println("统计总数:"+total);System.out.println("预期总数:"+LOOP_COUNT);System.out.println("计数是否准确:"+(total==LOOP_COUNT));}}

核心优化点说明

优化维度原始问题优化方案优化效果
锁机制全局synchronized锁(并发变串行)移除全局锁,使用compute()原子方法锁粒度缩小到单个Key,真正并发执行
操作效率三次哈希查找(containsKey+get+put)compute()一次原子操作完成计数逻辑减少哈希开销,逻辑更简洁
资源管理线程池可能泄漏try-finally包裹,确保线程池必关闭避免系统资源泄漏
代码健壮性无异常处理核心逻辑包裹在try块中鲁棒性提升

五、优化后性能测试

同样在8核CPU机器上运行优化后的代码,结果如下:

=== 优化后代码执行耗时统计 === 循环次数:10000000 线程数量:10 总耗时:260 毫秒(0.26 秒) === 统计结果 === item0: 999789 item1: 1000211 item2: 999956 ... 统计总数:10000000 预期总数:10000000 计数是否准确:true

性能对比

  • 原始代码:约1280ms(1.28秒)
  • 优化后代码:约260ms(0.26秒)
  • 性能提升:近5倍(不同机器略有差异)

六、进阶优化:超高并发场景(可选)

如果循环次数达到上亿次,可以进一步用LongAdder替代Long类型,LongAdder专为高并发计数设计,性能比直接自增Long更高:

// 替换Map定义privateMap<String,LongAdder>freqs=newConcurrentHashMap<>(ITEM_COUNT);// 计数逻辑改为freqs.computeIfAbsent(key,k->newLongAdder()).increment();// 最终取值时转换为LongMap<String,Long>result=freqs.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,e->e.getValue().longValue()));

七、总结

  1. 核心避坑点:使用ConcurrentHashMap时,避免对整个Map加全局锁,否则会完全丧失其并发优势;
  2. 最佳实践:优先使用ConcurrentHashMap内置的原子方法(compute()/merge()/computeIfAbsent())实现线程安全操作,锁粒度更细、性能更好;
  3. 资源管理:线程池等资源必须在finally块中确保关闭,避免资源泄漏;
  4. 性能优化:超高并发计数场景,优先选择LongAdder替代Long自增。

通过本次优化,我们不仅解决了代码的性能问题,也掌握了高并发下ConcurrentHashMap的正确使用方式,避免从“并发优化”变成“并发负优化”。

http://www.jsqmd.com/news/505231/

相关文章:

  • LabVIEW B样条曲线拟合
  • 革新性星穹铁道智能托管解决方案:三月七小助手全方位效率提升工具包
  • VideoAgentTrek Screen Filter 企业级应用:Java开发者的AI视频处理集成方案
  • 从N皇后问题看算法选择:回溯法与分支限界法的实战场景与模板精讲
  • Python+skfuzzy实战:用模糊PID控制打造智能温控系统(附完整代码)
  • letcode 19 删除链表中倒数第n个节点
  • 大型源码C# WPF开发框架:集成SCADA数据采集系统、数据库与远程服务器调用,包含多个产品...
  • 子比主题子比超级插件-带AI功能美化集合自助广告,工单,悬赏,团购,砍价等
  • GD32F303CG实战:I2C读写BL24C256A EEPROM的5个常见坑及解决方法
  • MinIO Operator v6.0.3 进阶部署:从本地磁盘规划到高可用 Tenant 配置详解
  • 多端同步不脱节,待办管理超省心
  • Infinite Prefixes (Codeforces- P1295B)
  • Bootstrap 5弹出框全攻略,虚幻基础:容器。
  • MQTTnet版本升级指南:从3.x到5.x的平滑迁移与关键注意事项
  • 18天解决“沟通不当”封号,完整申诉思路!
  • 告别‘盲写’代码:Replit Agent产品经理揭秘,AI编程助手如何从‘异步奴隶’进化成‘合作搭档’
  • 万能视频去水印软件视频去字幕工具视频工作者必备B站视频去水印工具 无损视频硬字幕去除工具 Ai视频去水印软件
  • Xilinx Virtex UltraScale+ VU19P FPGA:高密度逻辑与高速接口的完美融合
  • 视频PPT智能提取:让80%的重复工作时间成为历史
  • 机器人学基础笔记-具身智能基础与机器人控制
  • Qwen3-32B-Chat快速部署教程:Python3.10+PyTorch2.0+CUDA12.4环境零配置启动
  • Spring Cloud OpenFeign实战:两种方式优雅传递HTTP请求头(附完整代码示例)
  • 企业智脑是噱头?看数谷如何帮珠三角企业重构神经系统?
  • 开源工具gerbv:制造业图纸质量控制的精准验证与高效处理方案
  • Linux apt 命令详解
  • Qwen3.5-9B镜像方案:企业内网离线部署Qwen3.5-9B服务的完整流程
  • 20 Python 关联分析:数据量大了,Apriori 太慢怎么办?一文入门 FP-Growth 算法
  • 线阵相机选型与调试全攻略:海康工业相机在结构光应用中的最佳实践
  • LumiPixel Canvas Quest生成结果的一致性控制研究
  • Excel实战:多元线性回归预测房价全流程解析