skip to content

How do you decide whether parallelism will actually pay off, and how do ordering-sensitive operations like findAny and forEach behave in parallel?

level: principalimportance: should knowfreq 50%

answer

  1. N × Q model: many elements × real per-element cost
  2. need splittable source + non-blocking + measure (JMH)
  3. findAny = any match (fast, non-deterministic); findFirst = first (costly)
  4. forEach unordered in parallel; forEachOrdered re-adds coordination
  5. distinct/sorted/limit costlier in parallel; .unordered() relaxes it

basics

~20 s

Parallelism pays off when there are many elements (N) and each costs real work (Q) — a big N×Q. For tiny data or cheap operations the overhead loses. In parallel, findAny may return any matching element (not the first), and forEach runs in no guaranteed order; use findFirst or forEachOrdered if order matters.

solid answer

~50 s

Decide with the informal N×Q model: N is the number of elements and Q is the per-element cost. Parallelism only helps when N×Q is large enough that the saved compute outweighs the fixed costs of splitting, fork/join scheduling, and merging — a rough rule of thumb is you want on the order of tens of thousands of elements times nontrivial work. Other prerequisites: a well-splittable source (array/ArrayList, not LinkedList/iterate) and cheap, associative, non-blocking operations. Always benchmark, since boxing, memory bandwidth, and ordering costs distort intuition. On ordering: a parallel stream relaxes encounter-order semantics where the operation allows it. findAny may return any matching element — it lets any thread win, which is faster than findFirst, which must respect order. forEach gives no order guarantee in parallel (each chunk's thread emits as it finishes); forEachOrdered preserves encounter order but reintroduces coordination overhead. Operations like distinct, sorted, and limit are more expensive in parallel because they must reconcile order.

code

java · 13 lines
java
// findAny: any matching element, faster in parallel (non-deterministic which one)
Optional<Order> some = orders.parallelStream()
                             .filter(Order::isFlagged)
                             .findAny();

// forEach in parallel: NO order guarantee
names.parallelStream().forEach(System.out::println);        // interleaved

// forEachOrdered: encounter order preserved, but re-adds coordination cost
names.parallelStream().forEachOrdered(System.out::println); // ordered, slower

// drop ordering so distinct/limit can run cheaper when order is irrelevant
long d = data.parallelStream().unordered().distinct().count();

go deeper

for a junior

Understands parallel isn't automatically faster and that order can change.

for a middle

Knows findAny may return any match and forEach is unordered in parallel; uses findFirst/forEachOrdered when order matters.

for a senior

Applies the N×Q heuristic with splittability and non-blocking prerequisites, and explains the cost of order-sensitive ops.

for a principal

Treats parallelisation as a measured engineering decision across the system — benchmarks on real data/hardware, governs shared-pool contention, and chooses order-insensitive operations or alternative concurrency models deliberately.

## The decision: the N × Q model Going parallel is *not* free. Before any element is processed, the framework must split the source, schedule fork/join tasks, and later merge their results — all pure overhead. Parallelism is only a win when the *useful work* saved by running on multiple cores exceeds that overhead. A widely used informal model (popularised by Brian Goetz) is **N × Q**: - **N** = the number of elements in the stream. - **Q** = the cost of processing one element (how expensive the pipeline's per-element work is). The *total* work is proportional to N × Q, and that is what parallelism amortises the fixed overhead against. The practical guidance: you want **N × Q to be large** — many elements *and/or* substantial per-element cost. A rough heuristic is that you rarely benefit below ~10,000 elements of trivial work, but a small N can still win if Q is huge (e.g. each element triggers a heavy CPU computation). Conversely, a million-element stream doing a near-free operation (and worse, boxing primitives) often *loses* to sequential because overhead and memory traffic dominate. Other prerequisites stack on top of N × Q: 1. **Splittable source** — arrays, `ArrayList`, `IntStream.range` split cheaply and evenly; `LinkedList`, `Stream.iterate`, line readers do not (see the Spliterator topic). 2. **CPU-bound, non-blocking work** — blocking I/O on the common pool starves it. 3. **Cheap, associative merge** — a costly combine step (or order-reconciling ops) erodes the gain. 4. **Measure, don't guess** — boxing/unboxing, cache effects, and merge costs routinely invert naive expectations; benchmark with a tool like JMH. ## Ordering semantics change in parallel Streams have an **encounter order** (the order elements appear in the source, e.g. a `List`). Sequential streams naturally honour it. Parallel streams *relax* it wherever an operation permits, trading determinism for speed. ### findAny vs findFirst - **`findFirst()`** must return the *first* element in encounter order. In parallel that forces coordination — workers must agree on who is earliest — so it is comparatively expensive. - **`findAny()`** is allowed to return *any* matching element. In parallel, whichever worker finds a match first can win immediately, so it is faster and is the right choice when *any* match suffices. Its result is **non-deterministic** under parallelism (and may even differ run to run), which is by design. ### forEach vs forEachOrdered - **`forEach`** makes **no ordering guarantee** at all in parallel: each chunk's worker emits its elements as it finishes, so output is interleaved/unordered. Use it only when order is irrelevant. - **`forEachOrdered`** processes elements in encounter order even in parallel — but to do so it must buffer and synchronize, **reintroducing the very coordination parallelism tried to avoid**, partly defeating the point. ### Order-sensitive operations cost more in parallel `distinct()`, `sorted()`, and `limit()`/`skip()` are *stateful* and order-aware: in parallel they must reconcile results across chunks (e.g. dedupe across threads, or take a true prefix), which adds buffering and merging overhead. If you do not need encounter order, calling `.unordered()` on the stream lets these operations run cheaper. ## Putting it together (principal lens) Deciding to parallelise is an engineering judgement, not a reflex: estimate N × Q, confirm a splittable non-blocking source, prefer order-insensitive operations (`findAny`, `unordered`, plain `forEach` when safe), and **benchmark on representative data and hardware**. For latency-sensitive or shared-pool-contended services, weigh whether a dedicated executor or a different model is a better fit than leaning on the common pool.

  • Why might a parallel stream over a million boxed Integers be slower than the sequential version?
    With trivial per-element work Q, the N×Q useful work is small relative to the fixed split/schedule/merge overhead, and boxing/unboxing plus poor cache locality add memory-bandwidth pressure that parallelism cannot overcome — so overhead dominates and it loses to sequential. Using a primitive IntStream avoids the boxing tax.
  • When would you choose findFirst over findAny despite the performance cost?
    When the result must be deterministic and tied to encounter order — e.g. you need the first matching record by position, not just any match. findAny's freedom to return any element makes it non-deterministic in parallel, which is unacceptable when callers depend on a specific element.

saying these in an interview costs you the question

  • Sprinkling parallel() everywhere expecting free speedups without estimating N×Q or benchmarking.
  • Assuming findAny returns the first matching element — it returns any, non-deterministically in parallel.
  • Expecting forEach to preserve order in parallel; it does not (use forEachOrdered).
  • Forgetting that forEachOrdered/sorted/distinct/limit reintroduce coordination that erodes the parallel benefit.
  • Ignoring that boxing primitives can make a huge-N parallel stream slower than sequential.

context