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



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

对于矩阵:

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 方法获取特定行和列的双精度,get(行,列)set方法将给定的数字添加到该行和列的双精度。 此代码在整数级别工作正常,但是当涉及到精度很高的双精度时,对于相同的两个矩阵的乘法,它将有不同的答案,有时一个数字可以大到 0.5 到 1.5。为什么。

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

也就是说,虽然您使用java.util.ConcurrentHashMap可以保证MapAPI 调用中的线程安全,但它不会确保映射在调用之间不会更改,例如在您调用containsKey的时间和您调用put的时间之间,或者在您调用get的时间和您调用put的时间之间

。从 Java 8 开始(您使用 lambda 和流表明您正在使用它),纠正此问题的一种选择是通过computeAPI 调用使检查现有 + get + set 序列成为原子序列。compute允许您提供一个键和一个 lambda(或方法引用),指定如何改变映射到该键的值,ConcurrentHashMap保证包含完整检查和设置逻辑的 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版本,但当然,你的代码的其他部分已经依赖于Java 8。 @Sumitsu提供的解决方案利用了 Java 8 中新增的 API 功能;它比上面更优雅,但说明性较差。

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

最新更新