让我接触 Java 中的并发性,并在多线程中遇到了这个相当常见的问题。我有一段代码(如下),它只需要两个矩阵 m1 和 m2,并将 m1[i][j]
和 m2[i][j]
的和写入 result[i][j]
。
for(int i = 0; i < numCols ; i++) {
for(int j = 0 ; j < numRows ; j++) {
int finalI = i;
int finalJ = j;
executorService.execute(
new Runnable() {
@Override
public void run() {
ArrayList<Integer> v1 = m1.get(finalI);
Integer m1Val = v1.get(finalJ);
ArrayList<Integer> v2 = m2.get(finalI);
Integer m2Val = v2.get(finalJ);
result.get(finalI).add(finalJ, m1Val + m2Val);
}
}
);
}
}
数组是 ArrayLists<ArrayList<Integer>>
类型,其中每个嵌套的 ArrayList
描述一列。它们的尺寸为 numRows
x numCols
。我测量了这个操作的时间,将一对随机生成的大小为 10000 x 10000 的矩阵相加,发现单线程版本花了我 123 秒,多线程(6 核 intel i7 上的 11 个线程)版本花了我大约 300 秒。
在这种情况下,我选择使用 ArrayList,因为它们允许不安全的并发访问,即我可以同时修改 ArrayList 的不同部分。但是,这并没有提供我预期的任何额外加速。我对为什么没有看到加速的猜测是由于以下原因:
这些猜测有意义吗?我可能没有看到任何其他解释?
答案 0 :(得分:2)
您有两个主要问题:
ArrayLists<ArrayList<Integer>>
) 使用了一种非常低效的数据结构,数据局部性很差,访问单个项目的开销很大。1 和 2 都会导致许多额外的 CPU 周期完全被浪费掉;它们还会导致数据局部性差,导致缓存未命中数超过必要。
此外,您得到的结果不正确,因为您使用的是非线程安全的数据结构(在本例中为“ArrayList,因为它们允许不安全的并发访问”)来收集结果;如果它没有为每个结果预先填充 Integer
值,那么当列表扩展并覆盖之前的数据时,您将丢失数据。
一种有效的方法是:
Runnable
对整个部分执行加法。这意味着,如果您有 8 个内核和 8 个工作线程,那么每个线程将处理一个 Runnable,并且该 Runnable 对矩阵的 12.5% 执行加法。int[][]
,或者更好的是,使用 int[]
并自行计算 row * width + col
的索引。这提供了更好的数据局部性,并且不进行任何自动装箱和拆箱,从而也提高了速度。使用 int[]
特别适合添加矩阵,因为您可以将矩阵视为数组 - 您不需要了解行和列,只需 result[i] = m1[i] + m2[i];