The full picture
- The NQ heuristic: N (elements) × Q (cost per element) should be large (~10,000+ 'units') before parallelism pays for its splitting/merging overhead.
- Source splittability:
ArrayList, arrays,IntStream.rangesplit perfectly;Stream.iterateandBufferedReader.linessplit terribly. - Shared pool hazard: every parallel stream in the JVM shares commonPool (size = cores − 1). One team's blocking lambda throttles everyone. Workaround (submitting from a custom FJ pool) is undocumented behavior — treat as a smell.
- Merge costs:
collect(toMap)and orderedlimit/findFirstpay heavy merge/ordering taxes;groupingByConcurrent+ unordered sources ease this. - Accumulator safety: never mutate shared state in
forEach— usecollect(mutable reduction) which gives each thread its own container.
// DON'T: blocking I/O on the common pool
orders.parallelStream()
.map(o -> httpClient.enrich(o)) // blocks a commonPool worker!
.toList();
// DO: bounded executor (or virtual threads) for I/O-bound fan-out
try (var scope = StructuredTaskScope.open()) {
var tasks = orders.stream()
.map(o -> scope.fork(() -> httpClient.enrich(o)))
.toList();
scope.join();
}