Definition
- Mechanism:
parallelStream()orstream().parallel()usesSpliterator.trySplit()to divide data into partitions processed by the common ForkJoinPool. - Thread count:
availableProcessors() - 1worker 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?
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());
}
}
Total: Rs.373000
Pool size: 7
How do you use a custom ForkJoinPool?
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
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.
---