1. Recap: Stream API
You’re already familiar with the Stream API—it’s a convenient way to work with collections that lets you write compact, readable data-processing code: filtering, sorting, counting, and so on.
Here’s a classic example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n)
.sum();
System.out.println(sum); // 6 (2 + 4)
In this example, the collection is turned into a stream (stream()), only even numbers are selected, then they are converted to int, and the result is summed using sum().
The Stream API makes code shorter and more expressive: instead of describing step by step how everything happens, you simply state what you want to get. Moreover, if necessary, you can easily switch to parallel processing—with a single line.
2. Parallel streams: syntax and how they work
How to make a stream parallel?
It’s simple: use parallelStream() instead of stream(). Or call .parallel() on an existing stream.
List<Integer> numbers = ...;
int sum = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n)
.sum();
Or like this:
numbers.stream()
.parallel() // convert to a parallel stream
.filter(...)
.map(...)
.sum();
What happens under the hood?
- The collection is automatically split into parts.
- Each part is processed in a separate thread (it uses ForkJoinPool—a special thread pool).
- The results are combined into the final value.
That is, if you have a multi-core CPU, processing really goes in parallel—for example, filtering and summing can be performed simultaneously on several cores.
Where is this especially useful?
- Processing large collections (tens of thousands of elements or more).
- Expensive computations for each element.
- No need to preserve strict processing order.
Example: comparing sequential and parallel streams
Let’s look at a simple example processing a large array.
import java.util.*;
import java.util.stream.*;
public class ParallelStreamDemo {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000)
.boxed()
.collect(Collectors.toList());
// Sequential stream
long time1 = System.currentTimeMillis();
long count1 = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
long time2 = System.currentTimeMillis();
System.out.println("Sequential: " + (time2 - time1) + " ms, even numbers: " + count1);
// Parallel stream
long time3 = System.currentTimeMillis();
long count2 = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();
long time4 = System.currentTimeMillis();
System.out.println("Parallel: " + (time4 - time3) + " ms, even numbers: " + count2);
}
}
Try this code on your computer—most likely the parallel stream will process the collection faster (especially if you have a multi-core processor). But not always! More on the nuances below.
3. How it works: ForkJoinPool and automatic splitting
Parallel streams use under the hood ForkJoinPool.commonPool(), which automatically manages the number of threads (usually equal to the number of available CPU cores).
Schematic:
+-----------------------------+
| Your collection |
+-----------------------------+
| 1 | 2 | 3 | ... | 10 million |
+----+----+----+-----+--------+
| | | |
v v v v
[Thread1][Thread2]...[ThreadN]
| | | |
+----+----+-----------+
|
[Result aggregation]
Each thread processes its part, then the results are combined.
4. Limitations and pitfalls
Parallel streams are not a magic “speed up everything” button. Sometimes they even slow execution down!
When parallelism is not beneficial:
- The collection is small (up to ~1000 elements).
- The per-element operation is very fast (for example, just n * 2).
- You need strict processing order (for example, for sequential writes to a file).
Why? Creating and synchronizing threads also takes time. If the task itself is “small,” overhead can outweigh the benefits of parallelization.
Side effects are the enemy of parallelism
If your operations inside the stream mutate external variables, be careful!
Bad example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};
numbers.parallelStream().forEach(n -> sum[0] += n);
System.out.println(sum[0]); // ??? (you expect 15, but you can get anything)
Why? Because several threads change the same variable at the same time—this causes a race condition. The final value can be incorrect.
The correct approach is to use stream methods that return a result:
int sum = numbers.parallelStream().mapToInt(n -> n).sum();
Not all collections parallelize equally well
Some collections (for example, a regular ArrayList) split well. But LinkedList or an infinite stream (for example, Stream.generate(...))—not so much.
5. Practice: performance comparison
Example: finding the maximum number
import java.util.*;
import java.util.stream.*;
public class ParallelMaxDemo {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 30_000_000)
.boxed()
.collect(Collectors.toList());
// Sequential
long t1 = System.currentTimeMillis();
int max1 = numbers.stream().max(Integer::compareTo).get();
long t2 = System.currentTimeMillis();
System.out.println("Sequential: " + (t2 - t1) + " ms, max = " + max1);
// Parallel
long t3 = System.currentTimeMillis();
int max2 = numbers.parallelStream().max(Integer::compareTo).get();
long t4 = System.currentTimeMillis();
System.out.println("Parallel: " + (t4 - t3) + " ms, max = " + max2);
}
}
What will we see? On modern multi-core processors, a parallel stream is usually faster. But if you replace 30_000_000 with 1000, there will be no difference—and sometimes the parallel one is even slower!
6. Usage examples: filtering, aggregation, sorting
Filtering and counting
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace");
long count = names.parallelStream()
.filter(name -> name.length() == 4)
.count();
System.out.println("Names with length 4: " + count);
Grouping
List<String> words = Arrays.asList("cat", "whale", "cat", "dog", "whale", "cat");
Map<String, Long> freq = words.parallelStream()
.collect(Collectors.groupingBy(
w -> w,
Collectors.counting()
));
System.out.println(freq); // {dog=1, whale=2, cat=3}
Sorting (but parallelism doesn’t always give a speedup here!)
List<Integer> bigList = IntStream.rangeClosed(1, 5_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
List<Integer> sorted = bigList.parallelStream()
.sorted()
.collect(Collectors.toList());
long t2 = System.currentTimeMillis();
System.out.println("Parallel sort: " + (t2 - t1) + " ms");
7. Important nuances and recommendations
When to use parallelStream()
- The collection is large (tens of thousands of elements or more).
- The per-element operation is “heavy” (complex computations, file/network I/O).
- No dependency on element order.
- No side effects (external variables are not mutated).
When NOT to use parallelStream()
- The collection is small.
- The operation is fast.
- You need strict order preservation.
- There is access to shared variables (consider thread-safe collections or other approaches).
How to find out how many threads are used?
By default—equal to the number of CPU cores: Runtime.getRuntime().availableProcessors(). You can change this behavior via a system property:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");
Do this only if you understand the consequences—otherwise you can saturate the CPU and end up with slowdowns.
8. Common mistakes when working with parallel streams
Error #1: Side effects inside forEach
Many think: “I’ll fill a list in parallel now!”
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1_000)
.parallel()
.forEach(result::add); // DANGEROUS!
System.out.println(result.size()); // Result is nondeterministic!
Why is this bad? ArrayList is not thread-safe, and when adding concurrently from different threads, the result is unpredictable: there can be misses, duplicates, exceptions.
Solution: Use stream collection methods (collect) that ensure safety themselves, or special collections.
List<Integer> result = IntStream.range(0, 1_000)
.parallel()
.boxed()
.collect(Collectors.toList());
Error #2: Expecting a speedup on small tasks
Parallelism is not free! If the collection is small, a parallel stream can work slower due to overhead for scheduling and synchronization.
Error #3: Violating order
If element order matters to you (for example, when writing to a file), do not use parallel streams—order is not guaranteed (or it will work slower).
Error #4: Using unsuitable collections
Some collections (for example, LinkedList, non-standard structures) do not split well—parallelism efficiency drops.
Error #5: Ignoring thread safety when collecting results
If you collect results manually (for example, adding to a list), use thread-safe collections (CopyOnWriteArrayList, ConcurrentLinkedQueue) or stream collection methods.
GO TO FULL VERSION