skip to content

How do forEach and forEachOrdered differ, and what are the ordering and thread-safety pitfalls of forEach on parallel streams?

level: seniorimportance: should knowfreq 48%

answer

  1. forEach: no order guarantee; parallel = concurrent, arbitrary order
  2. forEachOrdered: encounter order even in parallel, at a perf cost
  3. Never accumulate into shared mutable state from parallel forEach (data race)
  4. Use collect/reduce for results; forEach only for independent side effects
  5. Consumer for parallel forEach must be stateless + thread-safe

basics

~20 s

forEach runs an action for each element but gives no order guarantee, and on a parallel stream it runs on multiple threads in any order. forEachOrdered always processes elements in encounter order, even in parallel (at a performance cost). Don't mutate shared state from forEach.

solid answer

~50 s

Both are terminal operations that perform a side effect per element and return void. forEach makes no ordering promise: on a sequential stream it typically follows encounter order, but on a parallel stream the action runs concurrently on multiple threads in an unspecified order. forEachOrdered guarantees encounter order regardless of parallelism, which forces synchronization between threads and usually erases the parallel speedup. The big pitfall is using forEach for accumulation: mutating a shared collection or variable from a parallel forEach is a data race and can corrupt state or lose updates, because the action runs on many threads with no coordination. The correct accumulation tool is collect (mutable reduction) or reduce, which are designed for safe parallel merging. Reserve forEach for genuine, independent side effects (logging, publishing), and use forEachOrdered only when you truly need ordered side effects.

code

java · 14 lines
java
List<Integer> nums = IntStream.rangeClosed(1, 8).boxed().toList();

// Order NOT guaranteed in parallel
nums.parallelStream().forEach(System.out::println);        // e.g. 5 6 1 2 ...

// Encounter order guaranteed (but serializes the output)
nums.parallelStream().forEachOrdered(System.out::println); // 1 2 3 ... 8

// BROKEN: data race on a non-thread-safe list
List<Integer> bad = new ArrayList<>();
nums.parallelStream().forEach(bad::add);   // may corrupt / lose elements

// CORRECT: mutable reduction, parallel-safe
List<Integer> good = nums.parallelStream().collect(Collectors.toList());

go deeper

for a junior

Knows forEach runs an action per element and returns nothing; forEachOrdered keeps order. Avoids mutating shared state from it.

for a middle

Explains that parallel forEach has no order guarantee and that accumulation belongs in collect/reduce, not forEach.

for a senior

Articulates the data-race mechanics, the forEachOrdered synchronization cost, and the reduction-over-side-effect design principle.

for a principal

Sets coding standards banning shared-state mutation in streams, evaluates when parallelism actually pays, and reviews for ordering/thread-safety correctness.

## Both are terminal, side-effecting, void `forEach` and `forEachOrdered` are **terminal operations**: they consume the stream and return **void** (no result). Their job is to perform a **side effect** — an action with an external consequence such as printing, logging, sending a message, or (incorrectly) mutating state. They take a **`Consumer<T>`** — a function that accepts one element and returns nothing. ## Encounter order — the key concept A stream may have an **encounter order**: the defined order in which elements appear (a `List` has one; a `HashSet` does not). Many ops preserve it. The two forEach variants differ precisely on whether they honor it: - **`forEach`** — makes **no ordering guarantee**. For a *sequential* stream it usually processes in encounter order as an implementation detail, but the spec does not promise it. For a *parallel* stream, the `Consumer` is invoked **concurrently on whatever thread processes each chunk, in an arbitrary order**. - **`forEachOrdered`** — **guarantees** the action is invoked in **encounter order**, even for a parallel stream. The runtime buffers/synchronizes so that, although elements may be *computed* in parallel, the *side effect* is applied strictly in order. ## The performance trade-off `forEachOrdered` on a parallel stream must coordinate threads to replay results in order. That synchronization typically **negates the benefit of going parallel**. So pairing `parallel()` with `forEachOrdered` is often pointless — you pay for parallelism and then serialize the output. ## Pitfall 1: relying on order from plain forEach Code that assumes `parallelStream().forEach(list::add)` preserves input order is wrong: parallel forEach order is unspecified. If you need ordered side effects, use `forEachOrdered`. ## Pitfall 2 (the serious one): shared mutable state Using `forEach` to **accumulate** into a shared structure is a **data race** in parallel: ```java List<Integer> out = new ArrayList<>(); nums.parallelStream().forEach(out::add); // BROKEN: ArrayList not thread-safe ``` `ArrayList` is not thread-safe; concurrent `add` calls can corrupt internal arrays, throw `ArrayIndexOutOfBoundsException`, or silently drop elements. Even with a synchronized collection you serialize on a lock (killing parallelism) and still rely on unspecified order. The **correct** pattern is a **reduction**, not a side effect: ```java List<Integer> out = nums.parallelStream().collect(Collectors.toList()); ``` `collect` performs a **mutable reduction**: each thread accumulates into its *own* container (via the Collector's supplier) and the **combiner** merges them — no shared mutation, correct in parallel. Likewise `reduce` folds with an associative operator. This is the heart of the rule: **streams want you to express results as reductions, not side effects.** ## When forEach IS appropriate forEach is fine for **independent side effects** where order and accumulation don't matter: writing each element to a logger, firing an event per element, calling an idempotent sink. The action must be **stateless and thread-safe** for parallel streams. ## Summary decision guide - Need a **result/collection** → `collect` or `reduce` (never forEach + shared mutation). - Need **independent side effects**, order irrelevant → `forEach` (ensure thread-safe consumer if parallel). - Need **ordered side effects** → `forEachOrdered` (accept the parallel cost or stay sequential).

  • Why is forEach(list::add) on a parallel stream dangerous, and what should you use instead?
    The Consumer runs concurrently on many threads mutating a non-thread-safe list, causing a data race (corruption or lost elements). Use collect(Collectors.toList()) (or reduce), which accumulates per-thread and merges via the combiner.
  • Does pairing parallel() with forEachOrdered make sense?
    Rarely. forEachOrdered must replay side effects in encounter order, forcing synchronization that usually cancels the parallel speedup. If you need ordered side effects, a sequential stream is often just as good.

forEach is handing the same task to a room of workers who each shout results whenever done — fast but chaotic order. forEachOrdered makes them report back strictly in line order — orderly but they end up waiting on each other, undoing the speed of working in parallel.

saying these in an interview costs you the question

  • Accumulating into a shared collection/variable from a parallel forEach
  • Assuming forEach preserves order (it does not, especially in parallel)
  • Using a synchronized collection with forEach and thinking it is now correct and fast
  • Believing forEachOrdered keeps the parallel speedup (it serializes the side effect)

context