Reputation: 13395
I want to perform parallel calculation for several sets of input values. Do I need to synchronize the calculate(a, b, inputIndex)
method?
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();
}
}
Because if the code of calculate
method was inside run
method of Runnable
I wouldn't need to do that. (also I think I don't need synchronized collections there because for input
I only get data by index and for output
I put element into the certain position)
Upvotes: 3
Views: 102
Reputation: 298153
It’s important to emphasize that trying the code and getting the correct output is not sufficient to prove the correctness of a program, especially not when multi-threading is involved. In your case it may happen to work by accident, for two reasons:
You have debug output statements, i.e. System.out.println(…)
, in your code, which is introducing thread synchronization, as in the reference implementation, PrintStream
synchronizes internally
Your code is simple and doesn’t run long enough to get deeply optimized by the JVM
Obviously, both reasons may be gone if you use similar code in a production environment.
For getting a correct program, even changing calculate(Integer a, Integer b, int inputIndex)
to a synchronized
method is not sufficient. Synchronization is only sufficient for establishing happens-before relationships regarding threads synchronizing on the same object.
Your initiating method calculate()
does not synchronize on the this
instance and it also doesn’t perform any other action that could be sufficient to establish a happens-before relationship with the calculating threads (like calling Thread.join()
. It only invokes Thread.sleep(4000)
, which obviously doesn’t even guaranty that the other threads completed within that time. Further, the Java Language Specification states explicitly:
It is important to note that neither
Thread.sleep
norThread.yield
have any synchronization semantics. In particular, the compiler does not have to flush writes cached in registers out to shared memory before a call toThread.sleep
orThread.yield
, nor does the compiler have to reload values cached in registers after a call toThread.sleep
orThread.yield
.For example, in the following (broken) code fragment, assume that
this.done
is a non-volatile
boolean
field:while (!this.done) Thread.sleep(1000);
The compiler is free to read the field
this.done
just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value ofthis.done
.
Note that what has been said in the example about this.done
applies to the array elements of the backing array of your list as well. Weren’t you using immutable String
instances, the effects could be even worse.
But it isn’t necessary to make the entire method synchronized
, only the data exchange must be thread safe. The cleanest solution is to make the entire method side effect free, i.e. turn the signature to String calculate(Integer a, Integer b)
and let the method return the result instead of manipulating a shared data structure. If the method is side effect free, it doesn’t need any synchronization.
The caller has to assemble the result values to a List
then, but since you are already using the Stream API, this operation comes for free:
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)
);
public void calculate() {
List<String> output = input.parallelStream()
.map(pair -> this.calculate(pair.getLeft(), pair.getRight()))
.collect(Collectors.toList());
System.out.println("Calculation result => " + output);
}
private String calculate(Integer a, Integer b) {
System.out.println(Thread.currentThread()+" does calculation of ("+a+","+b+")");
Evaluator eval = new Evaluator();
eval.putVariable("a", a.toString());
eval.putVariable("b", b.toString());
try {
String result = eval.evaluate(FORMULA);
Thread.sleep(3000);
System.out.println(Thread.currentThread()+" with ("+a+","+b+") done.");
return result;
} catch (EvaluationException | InterruptedException e) {
throw new RuntimeException(e);
}
}
Upvotes: 2