Back to Parallel Streams
Q1Question 1 of 10

Parallel stream: ForkJoinPool, when to use, caveats (ordering, small data, shared state)

Key Concepts
  • parallelStream() splits the source using Spliterator.trySplit() and processes partitions on ForkJoinPool.commonPool().
  • Default parallelism is availableProcessors minus 1 worker threads plus the caller thread.
  • All parallel streams in the JVM share ONE common ForkJoinPool: a slow pipeline blocks others.
  • Use a custom ForkJoinPool to isolate critical pipelines from the shared common pool.
  • Parallel streams are only beneficial for large datasets (10K+ elements) with CPU-bound per-element work.
  • forEach in parallel produces scrambled output; use forEachOrdered or toList() for ordered results.
  • ArrayList and arrays split evenly in O(1); LinkedList requires O(n) traversal to split, making parallel inefficient.

Definition

A parallel stream splits data into partitions processed by ForkJoinPool threads, beneficial for large CPU-bound datasets but harmful for small data, I/O-bound work, shared state, and order-sensitive operations.
  • Mechanism: parallelStream() or stream().parallel() uses Spliterator.trySplit() to divide data into partitions processed by the common ForkJoinPool.
  • Thread count: availableProcessors() - 1 worker threads plus the caller thread.
  • Shared pool: all parallel streams in the JVM share one ForkJoinPool; a slow stream blocks others.
  • Custom pool: new ForkJoinPool(n).submit(() -> stream...).get() isolates critical pipelines.
  • When beneficial: 10K+ elements, CPU-bound work, independent elements, well-splittable source (ArrayList, array).
  • When harmful: small datasets, I/O-bound, shared mutable state, LinkedList, order-critical forEach.

A sequential stream is one clerk processing 10,000 cheques at National Bank of India. A parallel stream is 8 clerks each taking approximately 1,250 cheques, processing simultaneously, then combining results. Fast for 10,000 cheques. But for only 10 cheques, hiring 8 clerks is overhead for no benefit. If all 8 clerks write to the same ledger simultaneously, entries get lost. All departments share the same pool of clerks: if accounting hogs them, loan processing waits.

Understanding the Concepts

What is a parallel stream?

A parallel stream splits data into partitions and processes them on multiple threads using the ForkJoinPool.

How does it work internally?

JAVA
package com.bank.service;

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

public class ParallelDemo {
    record Transaction(String id, BigDecimal amount, boolean settled) {}

    public static void main(String[] args) {
        List<Transaction> txns = List.of(
            new Transaction("TXN-001", new BigDecimal("45000"), true),
            new Transaction("TXN-002", new BigDecimal("78000"), true),
            new Transaction("TXN-003", new BigDecimal("12000"), false),
            new Transaction("TXN-004", new BigDecimal("250000"), true)
        );

        BigDecimal total = txns.parallelStream()
            .filter(Transaction::settled)
            .map(Transaction::amount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        System.out.println("Total: Rs." + total);
        System.out.println("Pool size: " + java.util.concurrent.ForkJoinPool.commonPool().getParallelism());
    }
}
TEXT
Total: Rs.373000
Pool size: 7

How do you use a custom ForkJoinPool?

JAVA
package com.bank.service;

import java.util.List;
import java.util.concurrent.ForkJoinPool;

public class CustomPoolDemo {
    public static void main(String[] args) throws Exception {
        List<String> accounts = List.of("SAV2001", "SAV2002", "CUR3001", "SAV2003");

        ForkJoinPool customPool = new ForkJoinPool(4);
        try {
            List<String> result = customPool.submit(() ->
                accounts.parallelStream()
                    .filter(a -> a.startsWith("SAV"))
                    .toList()
            ).get();
            System.out.println("Custom pool result: " + result);
        } finally {
            customPool.shutdown();
        }
    }
}

What are the caveats?

| Caveat | Problem | Fix |
|--------|---------|-----|
| Ordering | forEach scrambles output | Use forEachOrdered or toList() |
| Small data | Overhead exceeds benefit < 10K | Use sequential stream |
| Shared state | Race conditions | Use collect instead of forEach+add |
| Common pool | Slow stream blocks others | Use custom ForkJoinPool |
| Bad source | LinkedList splits in O(n) | Use ArrayList or array |
| I/O-bound | Threads sit idle | Use CompletableFuture or virtual threads |

When should you use parallel streams?

Use parallel streams when you have more than 10,000 elements, CPU-bound work per element, independent elements with no shared state, a well-splittable source like ArrayList, and order is not critical or you use collect/toList which preserves encounter order.

What should a senior engineer know?

For production systems, always use a custom ForkJoinPool for critical parallel pipelines to avoid starving the common pool. Profile with JMH before assuming parallel is faster. Consider virtual threads (Java 21) for I/O-bound parallelism instead of parallel streams.

Visual Overview

DIAGRAM
  Parallel Stream Pipeline:
  ─────────────────────────
  Source: [T1, T2, T3, T4, T5, T6, T7, T8]
           │
  trySplit: [T1-T4] → Thread-1    [T5-T8] → Thread-2
           │                         │
  filter+map per partition          filter+map per partition
           │                         │
  reduce locally                    reduce locally
           │                         │
           └──────── merge ──────────┘
                       │
                    Result

  Common Pool (shared!):
  ┌──────────────────────────────────┐
  │ parallel stream A (slow)        │ blocks
  │ parallel stream B (fast)        │ waits!
  │ parallel stream C               │ waits!
  └──────────────────────────────────┘
  Fix: custom ForkJoinPool per critical pipeline

Key Takeaway

A parallel stream splits data into partitions processed by ForkJoinPool threads, providing near-linear speedup for CPU-bound operations on large datasets. The six caveats are ordering, small datasets, shared mutable state, common pool starvation, poor source splittability, and I/O-bound work.

The decision: use parallel streams for 10K+ elements with CPU-bound work on well-splittable sources. Avoid for small datasets, I/O-bound work, and shared state. Use custom ForkJoinPool for production pipelines.

Remember one sentence: parallel streams split data across ForkJoinPool threads and are only faster for large CPU-bound datasets with independent elements.

---

Share
Use ← → arrow keys to navigate