ExerciseChallenge
Challenge
Four pipelines, one is worth parallelising
25 minintermediate2–12 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Parallel helps only with CPU-bound work, enough data and a cheap split
- A source that cannot split evenly gets no benefit — LinkedList and iterate
- Blocking IO on the common pool is the wrong mechanism, not a slow one
- A non-associative reduction changes its answer with the core count
- Measuring is part of the answer, not an optional extra
Starter
Starter.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
/**
* Challenge: four pipelines. Someone wants .parallel() on all of them.
*
* Exactly one is a good candidate. For each of the other three, the reason is
* different — and "it did not get faster" is not the reason for any of them.
*
* Decide first, measure second. Write your verdict in the comment before you
* touch the code.
*/
public class Starter {
/* ─────────── 1: a lot of cheap arithmetic over an array ─────────── */
static long sumOfSquares(int[] values) {
return IntStream.of(values).mapToLong(v -> (long) v * v).sum();
}
/* ─────────── 2: a remote call per element ─────────── */
static String lookup(String id) {
try {
Thread.sleep(5); // stands in for the network
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return id.toUpperCase();
}
static List<String> lookupAll(List<String> ids) {
return ids.stream().map(Starter::lookup).toList();
}
/* ─────────── 3: a short list, walked as a linked list ─────────── */
static int longestName(LinkedList<String> names) {
return names.stream().mapToInt(String::length).max().orElse(0);
}
/* ─────────── 4: a running total that is not a sum ─────────── */
static double compound(List<Double> rates) {
return rates.stream().reduce(1.0, (acc, r) -> acc * (1 + r) - 0.01);
}
public static void main(String[] args) {
int[] numbers = IntStream.range(0, 5_000_000).toArray();
List<String> ids = IntStream.range(0, 40).mapToObj(i -> "id-" + i).toList();
LinkedList<String> names = new LinkedList<>(List.of("ana", "bo", "chandrika", "dev"));
List<Double> rates = List.of(0.05, 0.03, 0.07, 0.02);
System.out.println("1 sumOfSquares : " + sumOfSquares(numbers));
System.out.println("2 lookupAll : " + lookupAll(ids).size() + " results");
System.out.println("3 longestName : " + longestName(names));
System.out.println("4 compound : " + compound(rates));
// TODO 1: verdict for each, BEFORE measuring. For every one write:
// CPU-bound? enough data? cheap split? shared state? associative?
//
// 1 sumOfSquares : ____
// 2 lookupAll : ____
// 3 longestName : ____
// 4 compound : ____
// TODO 2: exactly one of the four should get .parallel(). Add it there
// and nowhere else. Time both versions with System.nanoTime, run each
// three times, and record the numbers.
// TODO 3: number 2 gets faster with parallelStream and is still the
// wrong tool. Say what it costs the rest of the JVM, then write the
// version that does not — an ExecutorService sized for the remote
// service, not for the CPU.
// TODO 4: number 3 has two reasons not to parallelise. Name both. One
// is about the amount of data and one is about the data structure.
// TODO 5: number 4 is a correctness bug waiting for a bigger machine.
// Compute the result by hand for a split of [0.05, 0.03] and
// [0.07, 0.02], combining the halves. Compare with the sequential
// answer, then state the property the operator is missing.
// TODO 6: after measuring, say what you would tell the colleague who
// wanted parallel on all four. One sentence.
}
}Run it locally:
cd exercises/java/java8/parallel-stream-pitfalls/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
For each one, ask three questions: is the work CPU-bound, is there enough of it, and can the source be split cheaply?
Hint 2
How does a Stream.iterate source decide where to split? It cannot.
Hint 3
One pipeline is already correct and already fast. Adding parallel to it makes it slower — say why before you measure.
Hint 4
For the reduction, work out the answer by hand for two different splits.
Done when
- Each of the four has a written verdict with the reason
- Exactly one is parallelised, and you can defend it
- You measured rather than assumed, and recorded both numbers
- The non-associative reduction is identified and rewritten