Java 使用 join() 的多线程程序在计算相邻数字之和时给出了错误的结果

Java multi-threaded program using join() gives wrong results in calculating sum of adjacent numbers

我编写了这个简单的多线程程序来将 1 到 100,000 的数字相加。当我 运行 这个时,我得到了不同的值作为最终结果(值小于预期的 5000050000 )。当我只使用一个线程执行程序时,它给出了正确的结果。程序也适用于较小的值,例如 100。可能出了什么问题?提前致谢。

class Calculation {

  private long total=0;
  public void calcSum(long start, long end) {
      long i = start;
      for( ;i<=end; i++) {
          total += i;
      }
  }
  public long getTotal() {
     return total;
  }
}

class CalculatorThread extends Thread{
   private long start;
   private long end;
   private Calculation calc;
   public CalculatorThread(long start, long end, Calculation calc) {
       this.start = start;
       this.end = end;
       this.calc = calc;
   }
   @Override
   public void run() {
       calc.calcSum(start, end);
   }
}

public class ParallelTest {

  public static void main(String[] args) throws InterruptedException {

      int start = 1;
      int end = 100000;

      Calculation calc = new Calculation();
      CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
      CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);

      ct1.start();
      ct2.start();

      ct1.join();
      ct2.join();

      System.out.println(calc.getTotal());
  }
}

对共享可变状态的非同步访问通常不会很顺利。

在你的Calculation calc中,有一个可变变量long total。当您启动线程时:

  CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
  CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);

您在这两个线程之间共享 calc 的可变状态。 calc 内部没有任何同步,因此线程只会在随机时间间隔内相互浪费内存。

这是一个工作版本:

class ParallelSum {
  public static long calcSum(long start, long end) {
      long total = 0;
      for(long i = start; i < end; i++) {
          total += i;
      }
      return total;
  }

  public static class CalculatorThread extends Thread {
     private long result = 0;
     private long start;
     private long end;
     public CalculatorThread(long start, long end) {
         this.start = start;
         this.end = end;
     }
     @Override
     public void run() {
         result = calcSum(start, end);
     }
     public long getResult() {
         return result;
     }
  }

  public static void main(String[] args) throws InterruptedException {
    int start = 1;
    int end = 100000;
    int endExcl = end + 1;

    CalculatorThread ct1 = new CalculatorThread(start, endExcl/2);
    CalculatorThread ct2 = new CalculatorThread(endExcl / 2, endExcl);

    ct1.start();
    ct2.start();

    ct1.join();
    ct2.join();

    System.out.println(ct1.getResult() + ct2.getResult());
  }
}

输出:

5000050000

附加说明:始终使用 [inclusive, exclusive) 范围索引。这大大降低了差一错误的可能性。另外,我用一个方法替换了 Calculation class:方法内部的局部变量不会出错,可变状态越少越好。

total += i 不是原子的(参见 )。你有两个线程同时修改同一个值total,两个线程互相干扰

看看 AtomicLong (javadoc),它有一个方法 addAndGet 自动添加一些数字:

class Calculation {
    private AtomicLong total = new AtomicLong();
    public void calcSum(long start, long end) {
        long i = start;
        for( ;i<=end; i++) {
            total.addAndGet(i);
        }
    }
    public long getTotal() {
       return total.get();
    }
}

使用这种方法,两个线程都可以自动添加到 total,因此它们不会再干扰。

class Calculation 不是线程安全的,您在两个线程上使用同一个实例。所以每个线程都在替换另一个线程设置的值。

一个简单的解决方案是创建两个 Calculation 实例,每个实例对应 CalculatorThread

Calculation calc1 = new Calculation();
Calculation calc2 = new Calculation();
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc1);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc2);

这是一个 classic 计算,可以分解为独立的部分(可以并发执行)并在最后减少为单个结果。

这不是实现结果的最简单方法。如果你想计算从 1 到 N 的数字之和,你应该使用这个公式: 从 1 到 N = N(N+1)/2 的总和。您正面临所谓的不一致阅读。您的两个线程共享同一个字段 "calc",特别是计算的长字段总数 class。 使用您的解决方案,可能会发生这种情况:

trhead1 读取总数等于0, thread1 将 total 的值增加到 1, thread2 读取总数等于0, thread2 将 total 的值增加到 1.

您的解决方案也可能出现其他情况,事实是这样总计的值不是predictible.So使用提供的公式或简单的 for,不要对同步问题使用异步解决方案.

你的代码的根本问题是 Calculation 实际上是一个共享变量(count 字段)的包装器,它在没有任何同步的情况下被更新。

解决这个问题的方法有以下三种:

  • 同步变量;例如通过在适当的点使用 synchronized
  • 为变量使用原子类型
  • 不要使用共享变量。

从性能的角度来看,第三个选项更好,而且实现起来很简单;请参阅@JenniferP 的回答。

请注意,如果您为每个线程提供自己的 Calculation 对象,则不需要额外的同步。与 Thread::start()Thread::join() 调用关联的 happens before 关系确保主线程在子线程之后读取子线程时看到正确的结果加入。


就其价值而言,Calculation 的线程安全实现将在共享实例时运行,如下所示:

  class Calculation {

      private long total = 0;

      public void calcSum(long start, long end) {        
          for (long i = start; i <= end; i++) {
              increment(i);
          }
      }

      public synchronized long getTotal() {
         return total;
      }

      private synchronized void increment(long by) {
         total += by;
      }
  }

请注意,您需要同步 getter 和增量方法。另一种方法是使用 AtomicLong 来表示总数。但在这两种情况下,由于使用共享变量,都会产生大量开销。 (在 synchronized 的版本中,由于锁争用,开销可能会很大。如果与单线程版本相比有任何加速,我会感到惊讶。即使你可以消除线程创建开销。 )