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

为什么java concurrentmap在使用多线程计算double时略有不同,java,concurrency,Java,Concurrency,我尝试使用多线程方法进行矩阵乘法,但对于同一个矩阵,双精度之间的计算并不总是相同的。 代码如下: 对于矩阵: private ConcurrentMap<Position, Double> matrix = new ConcurrentHashMap<>(); public Matrix_2() {} public double get(int row, int column) { Position p = new Position(row, co

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

对于矩阵:

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);
    }
  }
私有ConcurrentMap矩阵=新ConcurrentHashMap();
公共矩阵_2(){}
公共双get(int行,int列){
位置p=新位置(行、列);
返回矩阵.getOrDefault(p,0.0);
}
公共无效集(整数行、整数列、双数值){
位置p=新位置(行、列);
if(矩阵containsKey(p)){
双a=矩阵。get(p);
a+=num;
矩阵put(p,a);
}否则{
矩阵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;
  }
公共静态矩阵乘法(矩阵a、矩阵b){
List threads=new ArrayList();
矩阵c=新矩阵_2();
IntStream.range(0,a.getNumRows()).forEach(r->
IntStream.range(0,a.getNumColumns()).forEach(t->
IntStream.range(0,b.getNumColumns())
弗雷奇先生(
v->
threads.add(新线程(()->c.set(r,v,b.get(t,v)*a.get(r,t())))
));
forEach(线程::start);
threads.forEach(r->{
试一试{
r、 join();
}捕捉(中断异常e){
系统输出打印项次(“坏”);
}
}
);
返回c;
}
其中get方法获取特定行和列的double,get(row,column)和set方法将给定的数字添加到该行和列的double中。
这段代码在整数级别工作得很好,但是当涉及到精度很高的双精度时,对于相同的两个矩阵的乘法,它将有不同的答案,有时对于一个数字,可能会大到0.5到1.5。这是为什么

虽然我还没有完全分析您的乘法代码,而且John Bollinger(在评论中)就浮点原语固有的舍入误差提出了一个很好的观点,但是您的
set
方法似乎有可能存在争用条件

也就是说,虽然您的使用保证了
Map
API调用中的线程安全,但它无法确保映射在调用之间不会发生更改,例如在调用时间和调用时间之间,或者在调用时间和调用时间之间

对于Java8(您使用的lambdas和streams表明您正在使用),纠正此问题的一个选项是通过使check existing+get+set序列原子化。允许您提供一个键和lambda(或方法引用),指定如何修改映射到该键的值,并保证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;
        }
    });
}

虽然我还没有完全分析您的乘法代码,并且John Bollinger(在评论中)就浮点原语固有的舍入误差提出了一个很好的观点,但是您的
set
方法似乎有可能存在争用条件

也就是说,虽然您的使用保证了
Map
API调用中的线程安全,但它无法确保映射在调用之间不会发生更改,例如在调用时间和调用时间之间,或者在调用时间和调用时间之间

对于Java8(您使用的lambdas和streams表明您正在使用),纠正此问题的一个选项是通过使check existing+get+set序列原子化。允许您提供一个键和lambda(或方法引用),指定如何修改映射到该键的值,并保证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;
        }
    });
}

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

事实上,您的
set()
方法甚至没有意义,至少在该名称下没有意义。如果实现确实是您想要的,那么它似乎更像是一种
increment()
方法

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

但是,如果必须保持计算结构不变,则需要更好的
set()
方法,该方法考虑到另一个线程更新
get()
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
}
这种方法适用于任何提供
ConcurrentMap
的Java版本,但当然,代码的其他部分已经依赖于Java8。@Sumitsu提供的解决方案利用了Java8中新增的API特性;它比上面的更优雅,但说明性不强


还要注意的是,在任何情况下看到结果中的微小差异都不会令人惊讶,因为对浮点操作重新排序可能会导致不同的舍入。

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