Java多线程计数异常求助:大数组统计结果偏差问题
修复多线程GradeCounter统计偏差问题
嘿,咱们来搞定你的多线程计数器在大数组下统计不准的问题!核心原因是竞态条件——虽然你用了ConcurrentHashMap保证了Map本身的线程安全,但GradeCount里的count++并不是原子操作,多个线程同时修改同一个GradeCount实例时会出现计数覆盖。另外还有个数组分割的小bug,会漏掉部分元素。下面一步步来修复:
1. 核心问题:非原子的count递增
count++看起来是一个操作,实际是三步:读取当前值、加1、写回内存。在多线程环境下,这三步可能被其他线程打断:比如两个线程同时读取到count=5,各自加1后都写回6,最终计数只增加了1,而不是预期的2。这就是大数组下统计结果偏小的根本原因。
2. 次要问题:数组分割不完整
当grades.length不能被nThreads整除时,最后一段的元素会被漏掉。比如数组长度55,nThreads=10,你计算的divide=5,最后一个线程只处理到索引50,而索引51-54的元素就被忽略了。
修复方案(简洁版,推荐)
我们可以直接用ConcurrentHashMap的merge方法来原子性统计计数,避免自定义类带来的线程安全问题,同时修复数组分割逻辑:
import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; public class GradeCounterImpl implements GradeCounter { public GradeCount[] count(String[] grades, int nThreads) { // 边界判断简化 if (grades == null || grades.length == 0) { return new GradeCount[0]; } // 直接用ConcurrentHashMap存字符串和对应的计数 ConcurrentHashMap<String, Integer> countMap = new ConcurrentHashMap<>(); ExecutorService exe = Executors.newFixedThreadPool(nThreads); int batchSize = grades.length / nThreads; for (int i = 0; i < nThreads; i++) { int start = i * batchSize; // 最后一个线程处理所有剩余元素,避免遗漏 int end = (i == nThreads - 1) ? grades.length : (i + 1) * batchSize; exe.execute(() -> { for (int j = start; j < end; j++) { String grade = grades[j]; if (grade != null) { // merge方法是原子操作:不存在则设为1,存在则累加1 countMap.merge(grade, 1, Integer::sum); } } }); } // 优化线程池关闭逻辑,确保资源释放 exe.shutdown(); try { if (!exe.awaitTermination(30, TimeUnit.MINUTES)) { exe.shutdownNow(); } } catch (InterruptedException e) { exe.shutdownNow(); Thread.currentThread().interrupt(); } // 转换为GradeCount数组并排序 return countMap.entrySet().stream() .map(entry -> new GradeCount(entry.getKey(), entry.getValue())) .sorted() .toArray(GradeCount[]::new); } } class GradeCount implements Comparable<GradeCount> { public String grade; public int count; public GradeCount(final String grade, final int count) { this.grade = grade; this.count = count; } @Override public int compareTo(final GradeCount other) { final int gradeCmp = this.grade.compareTo(other.grade); return gradeCmp == 0 ? Integer.compare(this.count, other.count) : gradeCmp; } }
关键修改点:
- 原子性计数更新:用
ConcurrentHashMap.merge()方法替代手动判断和更新,该方法内部已经实现了线程安全的原子操作,彻底解决竞态条件。 - 完整的数组分割:最后一个线程直接处理到数组末尾,确保没有元素被遗漏。
- 优化线程池管理:增加
shutdownNow()的 fallback 逻辑,避免线程池无法正常关闭,同时恢复线程中断状态。 - 简化结果转换:用Stream API更简洁地将统计结果转为
GradeCount数组,同时自动完成排序。
另一种方案(保留原GradeCount结构)
如果你想保留原来的GradeCount类结构,可以将count字段改为原子类AtomicInteger,确保递增操作是原子的:
修改GradeCount类:
class GradeCount implements Comparable<GradeCount> { public String grade; public AtomicInteger count; // 替换为原子类 public GradeCount(final String grade, final int count) { this.grade = grade; this.count = new AtomicInteger(count); } @Override public int compareTo(final GradeCount other) { final int gradeCmp = this.grade.compareTo(other.grade); return gradeCmp == 0 ? Integer.compare(this.count.get(), other.count.get()) : gradeCmp; } }
修改计数逻辑:
// 在WorkUnit的run方法中 for (String pos : positions) { if (pos != null) { // computeIfAbsent保证原子性创建GradeCount实例 GradeCount gc = save.computeIfAbsent(pos, k -> new GradeCount(k, 0)); // 原子性递增计数 gc.count.incrementAndGet(); } }
同时修复数组分割逻辑:
// 在count方法的循环中 int divide = grades.length / nThreads; for (int i = 0; i < nThreads; i++) { int start = i * divide; int end = (i == nThreads - 1) ? grades.length : (i + 1) * divide; exe.execute(new WorkUnit(arrayBetween(start, end, grades))); }
这两种方案都能彻底解决大数组多线程下的统计偏差问题,推荐第一种简洁版,代码更清晰且利用了ConcurrentHashMap的原生线程安全特性。
内容的提问来源于stack exchange,提问作者Selast Lambou
相关产品推荐
相关产品推荐

