Back to Parallel Streams
Q3Question 3 of 10

Sum transaction amounts safely with a parallel stream using an associative reduce

Key Concepts
  • A parallel stream splits data across threads using the fork-join framework and merges partial results
  • reduce(identity, accumulator, combiner) is the safe aggregation: accumulators fold per thread, combiner merges
  • The identity must satisfy combiner(identity, x) == x, and both functions must be associative and stateless
  • Associativity, not commutativity, is what parallel reduce requires
  • Subtraction and division are not associative, so they cannot be parallel reduce operations
  • Mutating a shared variable inside forEach in parallel is a data race and gives wrong, nondeterministic results
  • For mutable accumulation use collect with a proper concurrent collector, not hand-rolled shared state

Definition

In parallel, aggregate with `reduce` using an associative, stateless operation; never mutate a shared variable inside forEach.

Parallel aggregation is safe only when the work can be split and merged without changing the answer.

  • Parallel stream – splits the data across threads with the fork-join framework and combines partial results.
  • reduce(identity, accumulator, combiner) – the accumulator folds elements into a partial result within each thread, and the combiner merges those partials across threads.
  • Identity law – the identity must satisfy combiner(identity, x) == x, so starting a sub-sum from it changes nothing.
  • Associative and stateless – both functions must be associative and keep no external state, so regrouping the work does not change the result.
  • No shared mutable accumulator – updating one variable from many threads in forEach is a data race and must not be used.

To total a huge pile of transaction slips quickly at the National Bank of India, several clerks each add up a portion, then a supervisor adds the subtotals. reduce is that safe teamwork: subtotals combine into the grand total no matter how the pile was divided. Having all clerks scribble on one shared running total at once causes collisions and a wrong sum.

Understanding the Concepts

How does a parallel stream compute a sum?

A parallel stream partitions the elements using the fork-join framework, processes partitions on multiple threads, and merges the results. For aggregation the correct tool is reduce. The three-argument form has an accumulator that folds elements into a partial result within each thread and a combiner that merges partial results across threads.

JAVA
package com.bank.batch;

import java.math.BigDecimal;
import java.util.List;

public class ParallelSum {
    public static void main(String[] args) {
        List<BigDecimal> amounts = List.of(
            new BigDecimal("100.50"), new BigDecimal("200.25"),
            new BigDecimal("50.00"), new BigDecimal("75.75"));

        BigDecimal total = amounts.parallelStream()
            .reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

        System.out.println("total = " + total);
    }
}
TEXT
total = 426.50

What do the three arguments of reduce mean?

reduce(identity, accumulator, combiner) has three distinct roles. The identity is the starting value for each partial sum. The accumulator combines a partial result with one element. The combiner merges two partial results from different threads.

JAVA
// identity    = BigDecimal.ZERO      (start of each sub-sum)
// accumulator = (partial, element) -> partial.add(element)
// combiner    = (partialA, partialB) -> partialA.add(partialB)
BigDecimal total = amounts.parallelStream()
    .reduce(BigDecimal.ZERO,
            (partial, element) -> partial.add(element),  // within a thread
            (a, b) -> a.add(b));                          // across threads
System.out.println(total);

In a sequential stream the combiner is never called; it exists specifically so parallel partial results can be merged.

Why must the operation be associative?

Associativity means (a op b) op c == a op (b op c). The fork-join framework re-associates the work by splitting the data at arbitrary points, so the answer must not depend on where the splits happen. Addition is associative, so the sum is identical no matter how the data is divided.

JAVA
package com.bank.batch;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Stream;

public class AssociativityHolds {
    public static void main(String[] args) {
        List<BigDecimal> amounts = Stream.iterate(BigDecimal.ONE, b -> b.add(BigDecimal.ONE))
            .limit(1000).toList();

        BigDecimal seq = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal par = amounts.parallelStream().reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

        System.out.println("sequential = " + seq);
        System.out.println("parallel   = " + par);
        System.out.println("equal      = " + seq.equals(par));
    }
}
TEXT
sequential = 500500
parallel   = 500500
equal      = true

Why must the identity satisfy the identity law?

Each thread starts its partial sum from the identity. If combiner(identity, x) were not equal to x, every partial sum would be polluted by the starting value, and the more threads you use the more wrong the result becomes.

JAVA
// WRONG identity: using ONE instead of ZERO for addition
BigDecimal wrong = amounts.parallelStream()
    .reduce(BigDecimal.ONE, BigDecimal::add, BigDecimal::add);
// each partition adds an extra 1, so the result inflates with thread count

// CORRECT identity: ZERO is the true identity for addition
BigDecimal right = amounts.parallelStream()
    .reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

For addition the identity is ZERO; for multiplication it would be ONE.

Why is a shared mutable accumulator in forEach unsafe?

Updating one shared variable from many threads is a data race. Threads read, add, and write back concurrently, so updates are lost and the total is wrong and nondeterministic.

JAVA
package com.bank.batch;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Stream;

public class UnsafeForEach {
    public static void main(String[] args) {
        List<BigDecimal> amounts = Stream.generate(() -> BigDecimal.ONE)
            .limit(100_000).toList();

        // WRONG: shared mutable slot updated from many threads
        BigDecimal[] total = { BigDecimal.ZERO };
        amounts.parallelStream().forEach(a -> total[0] = total[0].add(a)); // data race

        System.out.println("unsafe total = " + total[0] + " (expected 100000)");
    }
}
TEXT
unsafe total = 73412 (expected 100000)

The number varies run to run and is almost never 100000, because concurrent reads and writes collide.

How do you fix the unsafe pattern?

Replace the shared mutable accumulator with reduce, which gives each thread its own partial result and merges them safely.

JAVA
package com.bank.batch;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Stream;

public class SafeReduce {
    public static void main(String[] args) {
        List<BigDecimal> amounts = Stream.generate(() -> BigDecimal.ONE)
            .limit(100_000).toList();

        // CORRECT: reduce with an associative operation, no shared state
        BigDecimal total = amounts.parallelStream()
            .reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

        System.out.println("safe total = " + total);
    }
}
TEXT
safe total = 100000

How does reduce contrast with forEach-plus-counter?

The difference is where state lives. reduce keeps per-thread partials that are merged; forEach-plus-counter shares one variable across threads.

| Aspect | reduce | forEach + shared counter |
|--------|----------|----------------------------|
| State ownership | per-thread partial results | one shared mutable variable |
| Thread safety | safe if associative and stateless | data race, unsafe |
| Determinism | same result every run | different result each run |
| Merge step | combiner merges partials | none, writes collide |
| Correct for parallel? | yes | no |

JAVA
// reduce: safe
BigDecimal safe = amounts.parallelStream().reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

// forEach + shared counter: unsafe, shown only as an anti-pattern
BigDecimal[] bad = { BigDecimal.ZERO };
amounts.parallelStream().forEach(a -> bad[0] = bad[0].add(a)); // do not do this

Why is associativity, not commutativity, the requirement?

The framework re-associates the work by splitting at different points, but for an ordered stream it preserves encounter order, so it never swaps operands. That means commutativity is not required, only associativity. Operations like subtraction and division are not associative, so they give wrong results in parallel reduce.

JAVA
package com.bank.batch;

import java.util.List;

public class SubtractionNotAssociative {
    public static void main(String[] args) {
        List<Integer> nums = List.of(10, 3, 2, 1);

        int seq = nums.stream().reduce(100, (a, b) -> a - b);
        int par = nums.parallelStream().reduce(100, (a, b) -> a - b);

        System.out.println("sequential = " + seq); // 84
        System.out.println("parallel   = " + par); // differs: subtraction is not associative
    }
}
TEXT
sequential = 84
parallel   = 110

The parallel result differs because (a - b) - c is not equal to a - (b - c).

When should you use collect instead of reduce for parallel aggregation?

reduce is ideal for immutable combination such as summing. When you need mutable accumulation, for example collecting into a container, use collect with a proper collector so the framework manages per-thread containers and merges them, instead of hand-rolling shared state.

JAVA
package com.bank.batch;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

public class CollectForMutable {
    public static void main(String[] args) {
        List<BigDecimal> amounts = List.of(
            new BigDecimal("100.50"), new BigDecimal("200.25"), new BigDecimal("50.00"));

        // summing with the built-in reducing collector
        BigDecimal total = amounts.parallelStream()
            .collect(Collectors.reducing(BigDecimal.ZERO, BigDecimal::add));
        System.out.println("total = " + total);
    }
}
TEXT
total = 350.75

What are common interview follow-up questions?

Expect: Why must a parallel reduce be associative? Answer: the fork-join framework re-associates work by splitting at arbitrary points, so the result must not depend on the grouping. Is commutativity required? Answer: no, encounter order is preserved for ordered streams, so only associativity matters. Why is subtraction a bad reduce operation in parallel? Answer: it is not associative, so the parallel result differs from the sequential one. Why is a shared counter in forEach wrong? Answer: concurrent reads and writes race and lose updates, giving a wrong, nondeterministic total. What should you use for mutable accumulation? Answer: collect with a proper concurrent collector, not hand-rolled shared state.

Visual Overview

DIAGRAM
PARALLEL REDUCE: SPLIT, FOLD PER THREAD, MERGE

                 amounts.parallelStream()
                          │ split (fork-join)
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
     ┌─────────┐    ┌─────────┐     ┌─────────┐
     │ thread 1│    │ thread 2│     │ thread 3│
     │ identity│    │ identity│     │ identity│
     │  + a+b  │    │  + c+d  │     │  + e+f  │   (accumulator)
     └────┬────┘    └────┬────┘     └────┬────┘
          │ partial 1    │ partial 2     │ partial 3
          └───────────────┼───────────────┘
                          ▼ combiner merges partials
                   ┌──────────────┐
                   │ grand total  │   same answer regardless of split
                   └──────────────┘


UNSAFE: SHARED MUTABLE COUNTER IN forEach

   thread 1 ─┐   read total=50 ─┐
   thread 2 ─┤                  ├─▶ both write back ─▶ one update LOST
   thread 3 ─┘   read total=50 ─┘                     wrong, nondeterministic


ASSOCIATIVE vs NOT

   add:        (a+b)+c == a+(b+c)   ✓ safe in parallel
   subtract:   (a-b)-c != a-(b-c)   ✗ wrong in parallel

Key Takeaway

A parallel stream splits the data across threads using the fork-join framework and then combines partial results, so for aggregation you use reduce, which is built for exactly this. The three-argument reduce takes an identity, an accumulator that folds elements into a partial result on each thread, and a combiner that merges those partials. For this to be correct the identity must be a true identity and both functions must be associative and stateless, so that however the framework divides the work, the answer is the same.

In our work at the National Bank of India, Rajesh totalled millions of end-of-day transaction amounts and wanted to use all cores within the batch window. He used a parallel stream with BigDecimal addition as both accumulator and combiner, and because addition is associative the total came out exact and identical on every run regardless of core count.

The point worth stressing is what makes parallel reduce safe and what breaks it. It is associativity, not commutativity, that parallelism requires, because the framework re-associates the work while preserving encounter order for ordered streams, and operations like subtraction and division are not associative so they cannot be parallel reduce operations.

Above all, never mutate a shared variable inside a forEach in a parallel stream, because concurrent reads and writes race and lose updates, producing a wrong, nondeterministic result. For mutable accumulation use collect with a proper concurrent collector instead, so the framework owns the per-thread containers and merges them safely.

Remember one sentence: in parallel, reduce with an associative, stateless operation and a true identity, and never let many threads scribble on one shared total.

---

Share
Use ← → arrow keys to navigate