Challenge

Four pipelines, one is worth parallelising

25 minintermediate212 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 Starter

Hints

  1. 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?

  2. Hint 2

    How does a Stream.iterate source decide where to split? It cannot.

  3. Hint 3

    One pipeline is already correct and already fast. Adding parallel to it makes it slower — say why before you measure.

  4. 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

← Back to When is parallelStream() a mistake?