Многопоточная Java-программа, использующая join (), дает неверные результаты при расчете суммы смежных чисел - PullRequest
0 голосов
/ 28 мая 2018

Я написал эту простую многопоточную программу для добавления чисел от 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());
  }
}

Ответы [ 5 ]

0 голосов
/ 28 мая 2018

Основная проблема в вашем коде заключается в том, что Calculation фактически является оберткой для общей переменной (поле count), которая обновляется без какой-либо синхронизации.

Существует три способа решенияэто:

  • синхронизировать переменную;например, используя synchronized в соответствующих точках
  • используйте атомарный тип для переменной
  • , не используйте общую переменную.

Третий вариант:лучше с точки зрения производительности, а реализация проста;см. ответ @ JenniferP.

Обратите внимание, что дополнительная синхронизация не требуется, если вы предоставляете каждому потоку свой собственный Calculation объект. происходит до того, как отношения , связанные с вызовами Thread::start() и Thread::join(), гарантируют, что основной поток увидит правильные результаты из дочерних потоков при чтении их после объединения.


Для чего стоит поточная безопасная реализация 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;
      }
  }

Обратите внимание, что вам нужно синхронизировать оба на геттереи метод приращения.Альтернативой является использование AtomicLong для представления итога.Но в обоих случаях будут значительные издержки из-за использования разделяемой переменной.(В версии с synchronized издержки могут быть чрезмерными из-за конфликта блокировок. Я был бы удивлен, если бы было какое-либо ускорение по сравнению с однопоточной версией. Даже если бы вы могли устранить накладные расходы на создание потоков.)

0 голосов
/ 28 мая 2018

это не самый простой способ добиться вашего результата.Если вы хотите вычислить сумму чисел от 1 до N, используйте следующую формулу: сумма от 1 до N = N (N + 1) / 2.Вы сталкиваетесь с тем, что называется непоследовательным чтением.Ваши два потока совместно используют одно и то же поле «calc» и особенно общее длинное поле класса «Расчет».С вашим решением это может произойти:

trhead1 читает итоговое значение, равное 0, thread1 увеличивает значение итогового значения до 1, thread2 читает итоговое значение, равное 0, thread2 увеличивает итоговое значение до 1.

Возможны другие сценарии с вашим решением, факт в том, что таким образом значение total не предсказуемо. Так что используйте предоставленную формулу или простое для, не используйте асинхронное решение для проблемы синхронизации.

0 голосов
/ 28 мая 2018

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 атомарно, поэтому они больше не будут мешать.

0 голосов
/ 28 мая 2018

Класс 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);

Этоклассический расчет, который может быть разбит на независимые части (которые могут выполняться одновременно) и сведен к одному результату в конце.

0 голосов
/ 28 мая 2018

Несинхронизированный доступ к общему изменяемому состоянию обычно не подходит.

В вашем 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 на метод: ничто не может пойти не так с локальными переменными внутри метода, и чем меньше изменяемое состояние, тем лучше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...