为什么java concurrentmap在使用多线程计算双精度时略有不同

时间:2017-03-13 19:06:08

标签: java concurrency

我正在尝试使用多线程方法进行矩阵乘法,但对于同一矩阵,双精度之间的计算并不总是相同。 有代码:

表示矩阵:

private  ConcurrentMap<Position, Double> matrix = new ConcurrentHashMap<>();

  public Matrix_2() {}


  public double get(int row, int column) {
    Position p = new Position(row, column);
    return matrix.getOrDefault(p, 0.0);
  }
  public void set(int row, int column, double num) {
    Position p = new Position(row, column);
    if(matrix.containsKey(p)){
      double a = matrix.get(p);
      a += num;
      matrix.put(p, a);
    }else {
      matrix.put(p, num);
    }
  }

用于乘法

    public static Matrix multiply(Matrix a, Matrix b) {
    List<Thread> threads = new ArrayList<>();
    Matrix c = new Matrix_2();
    IntStream.range(0, a.getNumRows()).forEach(r ->
      IntStream.range(0, a.getNumColumns()).forEach(t ->
          IntStream.range(0, b.getNumColumns())
              .forEach(
                  v -> 
           threads.add(new Thread(() -> c.set(r, v, b.get(t, v) * a.get(r, t)))))
      ));
    threads.forEach(Thread::start);
    threads.forEach(r -> {
        try {
          r.join();
        } catch (InterruptedException e) {
          System.out.println("bad");
        }
      }
    );
    return c;
  }

其中 get 方法获取特定行和列的double, get(row,column) set 方法添加给定的该行和列的数字为double。 这个代码在整数级别上运行良好,但是当它具有很多精度的double时,对于相同的两个矩阵的乘法将有不同的答案,对于一个数字,有时可以大到0.5到1.5。这是为什么。

2 个答案:

答案 0 :(得分:2)

虽然我还没有完全分析multiply的代码,而John Bollinger对于浮点基元固有的舍入误差(评论中)提出了一个很好的观点,您的set方法似乎有可能的竞争条件。

即,虽然您使用java.util.ConcurrentHashMap可以保证 Map API调用中的线程安全,但它无法确保映射无法在之间进行更改调用,例如在您调用containsKey的时间和调用put的时间之间,或者在您调用get的时间与调用{的时间之间的调用{3}}

从Java 8开始(你使用lambdas和streams表明你正在使用它),纠正这个问题的一个选择是通过put使check-existing + get + set sequence成为原子。 compute API call允许您提供一个键和一个lambda(或方法引用),指定如何改变映射到该键的值,compute保证lambda,它包含您的完整检查和设置逻辑,将以原子方式执行。使用这种方法,您的代码看起来像:

public void set(int row, int column, double num) {
    Position p = new Position(row, column);
    matrix.compute(p, (Position positionKey, Double doubleValue)->{
        if (doubleValue == null) {
            return num;
        } else {
            return num + doubleValue;
        }
    });
}

答案 1 :(得分:0)

并发集合可以帮助您编写线程安全的代码,但它不会使您的代码自动线程安全。您的代码 - 主要是您的set()方法 - 线程安全。

事实上,你的set()方法甚至没有意义,至少不是那个名字。如果实现确实是你想要的,那么它似乎更像是increment()方法。

我的第一个建议是通过消除中间IntStream来简化您的方法,或者至少将其移入线程中。这里的目标是避免任何两个线程操纵地图的相同元素。然后,您还可以结合使用真正的 set()方法,因为不会对单个矩阵元素进行争用。在这种情况下,ConcurrentMap仍然有用。很可能也会跑得更快。

但是,如果必须保持计算结构相同,那么您需要一个更好的set()方法,该方法可以解释另一个线程更新get()和{之间的元素的可能性{1}}。如果您没有考虑到这一点,那么更新可能会丢失。这些方面的一些改进是:

put()

该方法适用于提供public void increment(int row, int column, double num) { Position p = new Position(row, column); Double current = matrix.putIfAbsent(p, num); if (current != null) { // there was already a value at the designated position double newval = current + num; while (!matrix.replace(p, current, newval)) { // Failed to replace -- indicates that the value at the designated // position is no longer the one we most recently read current = matrix.get(p); newval = current + num; } } // else successfully set the first value for this position } 的任何Java版本,但当然,代码的其他部分已经依赖于Java 8. @Sumitsu提供的解决方案利用了Java 8中新增的API功能;它比上面的更优雅,但不太具有说明性。

另请注意,在任何情况下看到结果中的差异都不足为奇,因为重新排序浮点运算会导致不同的舍入。