我想对几组输入值执行并行计算。我是否需要同步calculate(a, b, inputIndex)
方法?
private static final String FORMULA = "(#{a} + #{b}) * (#{a} + #{b} * #{b} - #{a})";
private List<Pair<Integer, Integer>> input = Arrays.asList(
new ImmutablePair<>(1, 2),
new ImmutablePair<>(2, 2),
new ImmutablePair<>(3, 1),
new ImmutablePair<>(4, 2),
new ImmutablePair<>(1, 5)
);
private List<String> output = new ArrayList<>(Arrays.asList("", "", "", "", ""));
public void calculate() {
IntStream.range(0, input.size()).forEach(idx -> {
Pair<Integer, Integer> pair = input.get(idx);
Thread threadWrapper = new Thread(
() -> this.calculate(pair.getLeft(), pair.getRight(), idx)
);
threadWrapper.start();
});
try {
Thread.sleep(4000); // waiting for threads to finish execution just in case
System.out.println("Calculation result => " + output);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void calculate(Integer a, Integer b, int inputIndex) {
System.out.println("Thread with index " + inputIndex + " started calculation.");
Evaluator eval = new Evaluator();
eval.putVariable("a", a.toString());
eval.putVariable("b", b.toString());
try {
String result = eval.evaluate(FORMULA);
Thread.sleep(3000);
output.set(inputIndex, result);
System.out.println("Thread with index " + inputIndex + " done.");
} catch (EvaluationException | InterruptedException e) {
e.printStackTrace();
}
}
因为如果calculate
方法的代码位于run
Runnable
方法内,我就不需要这样做了。 (我也认为我不需要同步集合,因为input
我只能通过索引获取数据,而output
我将元素放入特定位置)
答案 0 :(得分:2)
重要的是要强调,尝试代码并获得正确的输出不足以证明程序的正确性,尤其是在涉及多线程时。在您的情况下,它可能偶然发生,有两个原因:
您的代码中有调试输出语句,即System.out.println(…)
,这引入了线程同步,如参考实现中所述PrintStream
内部同步
您的代码很简单,运行时间不够长,无法通过JVM进行深度优化
显然,如果在生产环境中使用类似的代码,这两个原因可能都会消失。
要获得正确的程序,即使将calculate(Integer a, Integer b, int inputIndex)
更改为synchronized
方法也是不够的。同步仅足以在同步的线程上建立发生前关系。
您的初始化方法calculate()
未在this
实例上进行同步,并且它也不执行任何足以建立发生在之前关系的其他操作使用计算线程(比如调用Thread.join()
。它只调用Thread.sleep(4000)
,这显然甚至不能保证其他线程在那段时间内完成。此外,Java Language Specification states explicitly:
值得注意的是,
Thread.sleep
和Thread.yield
都没有任何同步语义。特别是,在调用Thread.sleep
或Thread.yield
之前,编译器不必将寄存器中缓存的写入刷新到共享内存,编译器调用{之后也不必重新加载缓存在寄存器中的值{1}}或Thread.sleep
。例如,在以下(损坏的)代码片段中,假设
Thread.yield
是非this.done
volatile
字段:boolean
编译器只需一次读取字段
while (!this.done) Thread.sleep(1000);
,并在循环的每次执行中重用缓存值。这意味着循环永远不会终止,即使另一个线程更改了this.done
的值。
请注意,关于this.done
的示例中所说的内容也适用于列表的后备数组的数组元素。如果你不使用不可变this.done
个实例,效果会更糟。
但是没有必要创建整个方法String
,只有数据交换必须是线程安全的。最干净的解决方案是使整个方法副作用免费,即将签名转为synchronized
并让方法返回结果而不是操纵共享数据结构。如果该方法没有副作用,则不需要任何同步。
调用者必须将结果值汇总到String calculate(Integer a, Integer b)
,但由于您已经在使用Stream API,因此该操作是免费的:
List