Java多线程Leibniz级数计算π结果异常,单线程正常
问题分析与修复
核心原因:条件收敛级数的并行求和问题
Leibniz公式对应的级数是条件收敛级数,这类级数的特点是:改变项的求和顺序会导致最终结果发生变化。单线程下严格按照顺序累加项,结果正确;但多线程并行时,多个线程随机地对共享的pi变量进行加减操作,相当于打乱了级数的求和顺序,因此每次运行的结果都会随机变化。
代码中的其他线程安全问题
除了级数本身的特性问题,代码还存在以下线程安全缺陷:
- 静态变量
flag的修改与访问未同步:多个线程同时修改flag,存在竞态条件,且主线程提交任务时的PICal.flag判断无法保证线程及时停止。 - 线程未在循环中检查停止信号:线程发现
term小于阈值后仅设置flag就返回,但其他线程不会主动检查flag状态,会继续累加不需要的小项,导致结果错误。 - 共享变量
pi的依赖:虽然job方法用synchronized保证了原子修改,但无法解决求和顺序错误的核心问题。
修复方案:分区间独立计算后汇总
要在多线程下正确计算,需保证每个线程独立计算自己负责的连续项的子和(按顺序累加),最后将所有子和汇总。这样既利用多线程提升效率,又保证级数的求和顺序正确。
修改后的代码示例:
import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class PiCalculator { public static class PICal implements Callable<BigDecimal> { private final long start; private final long end; private final MathContext mc; public PICal(long start, long end, MathContext mc) { this.start = start; this.end = end; this.mc = mc; } @Override public BigDecimal call() { BigDecimal subSum = BigDecimal.ZERO; boolean add = start % 2 == 0; // 第start项的符号:偶数项加,奇数项减 for (long i = start; i < end; i++) { BigDecimal term = BigDecimal.ONE.divide(BigDecimal.valueOf(2L * i + 1), mc); if (add) { subSum = subSum.add(term); } else { subSum = subSum.subtract(term); } add = !add; } return subSum; } } public static void main(String[] args) { int floatingPoint = 8; BigDecimal base = new BigDecimal("0.1"); BigDecimal floatingPointThreshold = base.pow(floatingPoint + 1); MathContext mc = new MathContext(floatingPoint + 1); // 预先计算需要迭代的最大次数 BigDecimal maxDenominator = BigDecimal.ONE.divide(floatingPointThreshold, mc); long maxIter = (maxDenominator.longValue() - 1) / 2; int threadCount = 100; long power = maxIter / threadCount; ExecutorService executor = Executors.newFixedThreadPool(threadCount); List<Future<BigDecimal>> futures = new ArrayList<>(); // 分配任务区间 for (int i = 0; i < threadCount; i++) { long start = i * power; long end = (i == threadCount - 1) ? maxIter : (i + 1) * power; futures.add(executor.submit(new PICal(start, end, mc))); } // 汇总所有子和 BigDecimal piSum = BigDecimal.ZERO; for (Future<BigDecimal> future : futures) { try { piSum = piSum.add(future.get()); } catch (Exception e) { throw new RuntimeException(e); } } executor.shutdown(); BigDecimal pi = piSum.multiply(BigDecimal.valueOf(4)).setScale(floatingPoint, RoundingMode.FLOOR); System.out.println(pi); } }
修改说明:
- 用Callable代替Runnable:每个线程返回自己计算的子和,避免共享变量的竞态条件。
- 预先计算迭代次数:根据阈值算出需要处理的最大
i,避免线程中途判断的混乱。 - 分区间独立计算:每个线程处理连续的项区间,保证子区间内的求和顺序正确,最终汇总结果符合Leibniz公式的要求。
- 移除共享静态变量:所有线程的计算状态独立,仅在最后汇总结果,彻底解决线程安全问题。
内容的提问来源于stack exchange,提问作者seyed
相关产品推荐
相关产品推荐

