并行流统计Human数组高频姓名为何存在数据竞争?
并行统计成人姓名频率的数据竞争问题分析
嘿,我来帮你拆解这段代码里的数据竞争问题——虽然你用了ConcurrentHashMap,但还是踩了并行流里的常见坑!
问题根源:复合操作的非原子性
你在forEach里写的这段计数逻辑是多个操作的组合,并不是原子性的:
nameMap.put(p, nameMap.containsKey(p)? nameMap.get(p)+1 :1)
咱们把这个操作拆成三步看:
- 第一步:调用
nameMap.containsKey(p)检查当前姓名是否在map中 - 第二步:如果存在,调用
nameMap.get(p)获取当前计数,再加1得到新值;如果不存在,新值就是1 - 第三步:调用
nameMap.put(p, 新值)把新计数存回map
虽然ConcurrentHashMap的单个方法(比如containsKey、get、put)都是线程安全的,但这三个步骤组合起来就不是原子操作了。当多个并行线程同时处理同一个姓名时,就会出现「丢失更新」的问题:
举个实际场景:线程A和线程B同时处理姓名"张三",此时map里张三的计数是5。
- 线程A执行
containsKey和get,拿到计数5,计算出新值6- 线程B在这之后也执行
containsKey和get,同样拿到计数5,计算出新值6- 线程A先执行
put,把张三的计数改成6- 线程B接着执行
put,又把计数改成6
原本正确的计数应该是7,但现在变成了6,一次计数就这么丢失了!
怎么修复?用原子性的merge方法
ConcurrentHashMap专门提供了原子性的merge方法,可以完美解决这个问题。把forEach里的代码改成这样:
.forEach(p -> nameMap.merge(p, 1, Integer::sum))
merge方法会原子性地完成「检查是否存在→不存在就设为1→存在就把旧值加1」的整个流程,完全避免了数据竞争的问题。
额外小建议
最后找最频繁姓名的那段代码,可以简化一下:不用再开并行流(此时map的规模已经不大了),而且比较器用Comparator.comparingInt可读性更好:
return nameMap.entrySet() .stream() .max(Comparator.comparingInt(Map.Entry::getValue)) .map(Map.Entry::getKey) .orElse(null); // 可根据业务需求返回默认值
内容的提问来源于stack exchange,提问作者KateS
相关产品推荐
相关产品推荐

