我正在尝试使用多线程方法进行矩阵乘法,但对于同一矩阵,双精度之间的计算并不总是相同。 有代码:
表示矩阵:
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。这是为什么。
答案 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功能;它比上面的更优雅,但不太具有说明性。
另请注意,在任何情况下看到结果中的小差异都不足为奇,因为重新排序浮点运算会导致不同的舍入。