我认为Java memory model 的当前保证使得很难在编译器或 VM 级别上做很多(如果有的话)自动并行化。 Java 语言没有语义来保证任何数据结构甚至是有效的不可变的,或者任何特定的语句都是纯粹的并且没有副作用,因此编译器必须自动计算出这些以实现并行化。在编译器中可以推断出一些基本的机会,但一般情况将留给运行时,因为动态加载和绑定可能会引入编译时不存在的新突变。
考虑以下代码:
for (int i = 0; i < array.length; i++) {
array[i] = expensiveComputation(array[i]);
}
如果expensiveComputation 是一个pure function,其输出仅取决于它的参数,并且如果我们可以保证array 在循环期间不会被更改,那么并行化将是微不足道的(实际上我们是更改它,设置array[i]=...,但在这种特殊情况下,总是首先调用expensiveComputation(array[i]),所以这里没问题 - 假设array 是本地的并且没有从其他任何地方引用)。
此外,如果我们像这样改变循环:
for (int i = 0; i < array.length; i++) {
array[i] = expensiveComputation(array, i);
// expensiveComputation has the whole array at its disposal!
// It could read or write values anywhere in it!
}
那么即使expensiveComputation 是纯的并且不会改变它的参数,并行化也不再是微不足道的了,因为并行线程会在其他人正在阅读它的同时改变array 的内容!并行器必须在各种条件下找出数组expensiveComputation 的哪些部分,并相应地进行同步。
也许检测所有可能发生的突变和副作用并在并行化时将其考虑在内并不是完全不可能,但它会非常 > 很难,当然,在实践中可能是不可行的。这就是为什么并行化以及弄清楚一切仍然正常工作是 Java 程序员头疼的原因。
函数式语言(例如 JVM 上的 Clojure)是该主题的热门答案。纯粹的、无副作用的函数与persistent(“实际上是不可变的”)数据结构一起可能允许隐式或几乎隐式的并行化。让我们将数组的每个元素加倍:
(map #(* 2 %) [1 2 3 4 5])
(pmap #(* 2 %) [1 2 3 4 5]) ; The same thing, done in parallel.
这是透明的,因为有两件事:
-
#(* 2 %) 函数是纯函数:它接受一个值并输出一个值,仅此而已。它不会改变任何东西,它的输出只取决于它的参数。
- 矢量
[1 2 3 4 5] 是不可变的:无论谁在看,什么时候看,都是一样的。
在 Java 中可以创建纯函数,但是 2) 不变性是这里的致命弱点。 Java 中没有不可变的数组。 学究起来,nothing 在 Java 中是不可变的,因为即使 final 字段也可以使用反射来更改。因此不能保证计算的输出(或输入!)不会被并行化改变 -> 所以自动并行化通常是不可行的。
由于不变性,愚蠢的“加倍元素”示例扩展到任意复杂的处理:
(defn expensivefunction [v x]
(/ (reduce * v) x))
(let [v [1 2 3 4 5]]
(map (partial expensivefunction v) v)) ; pmap would work equally well here!