Parallel Streams in Java

Naveen AnamaneniUpdated · 5 min read

A parallel stream in Java splits its elements into parts and processes the parts at the same time on several CPU cores. You create one with collection.parallelStream() or by calling .parallel() on a stream. Behind the scenes, it uses the JDK's shared ForkJoinPool.commonPool(). Parallel streams can make big, CPU-heavy calculations much faster, but on small data, or with the wrong operations, they are slower and can even give wrong results. In this lesson, you will learn how they work and how to decide whether to use them.

Syntax

Syntax
list.parallelStream()...          // from a collection
stream.parallel()...              // switch an existing stream
stream.sequential()...            // switch back
stream.isParallel()               // check

How a Parallel Stream Works

  1. Split: the source is divided into chunks using a Spliterator.
  2. Process: each chunk goes through the pipeline on a different worker thread of the common pool.
  3. Combine: the partial results are merged into the final result.

The common pool has, by default, one fewer worker than the number of CPU cores, and the calling thread helps too.

Split the data, work on the chunks at once, then combine the partial results

Example: A Parallel Sum

Let's see an example that sums the square roots of 20 million numbers both ways. The times depend on the computer, so the output is a sample from a 10-core laptop.

ParallelSum.java▶ Try it
import java.util.stream.LongStream;
 
public class ParallelSum {
    static double work(boolean parallel) {
        LongStream s = LongStream.rangeClosed(1, 20_000_000);
        if (parallel) {
            s = s.parallel();
        }
        return s.mapToDouble(Math::sqrt).sum();
    }
 
    public static void main(String[] args) {
        work(false);
        work(true);
 
        long t = System.nanoTime();
        double a = work(false);
        long seq = (System.nanoTime() - t) / 1_000_000;
 
        t = System.nanoTime();
        double b = work(true);
        long par = (System.nanoTime() - t) / 1_000_000;
 
        System.out.printf("same result: %b%n", Math.abs(a - b) < 1e-3 * a);
        System.out.println("sequential: " + seq + " ms");
        System.out.println("parallel:   " + par + " ms");
        System.out.println("cores: "
            + Runtime.getRuntime().availableProcessors());
    }
}
Output
same result: true
sequential: 37 ms
parallel:   5 ms
cores: 10

Explanation: Each method is run once first, to let the JVM warm up. The parallel version spreads the 20 million calculations over all cores and finishes several times faster. The sums are compared with a small tolerance, because adding double values in a different order can change the last few digits.

When Parallel Streams Help and When They Hurt

HelpsHurts
Many elements (tens of thousands or more)Few elements
Expensive work per element (math, parsing)Cheap work per element
Sources that split well: ArrayList, arrays, IntStream.rangeSources that split badly: LinkedList, Stream.iterate, I/O
Stateless, independent operationssorted, limit, findFirst, forEachOrdered on ordered data
CPU-bound workWaiting work (network, database, files)

Example: A Parallel Stream Giving the Wrong Answer

Shared mutable state breaks parallel streams. Adding to an ArrayList from many threads loses elements; the exact numbers change on every run:

Unsafe.java▶ Try it
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
 
public class Unsafe {
    public static void main(String[] args) {
        List<Integer> unsafe = new ArrayList<>();
        try {
            IntStream.range(0, 100_000).parallel().forEach(unsafe::add);
            System.out.println("unsafe size: " + unsafe.size());
        } catch (RuntimeException e) {
            System.out.println("unsafe failed: "
                + e.getClass().getSimpleName());
        }
 
        List<Integer> safe = IntStream.range(0, 100_000).parallel()
            .boxed().toList();
        System.out.println("safe size:   " + safe.size());
    }
}
Output
unsafe size: 14396
safe size:   100000

Explanation: Many threads called add on the same ArrayList at the same time, so updates collided and elements were lost (sometimes the list even throws). toList() gives each thread its own partial list and merges them safely, so all 100,000 elements arrive.

Common Mistakes

Making every stream parallel "for speed". Most collections in everyday code are too small to benefit.

Doing blocking I/O in a parallel stream. It ties up the common pool that other parts of the program share.

Using forEach with shared state. Use collectors, which merge results safely.

Expecting forEach to keep the order. Use forEachOrdered if order matters.

Interview Corner

When should you use parallel streams?

When the data set is large, the work per element is CPU-intensive and independent, the source splits efficiently (arrays, ArrayList, ranges), and the operations are stateless and free of side effects. Measure before and after. Avoid them for small collections, I/O-bound work, sources such as LinkedList or Stream.iterate, order-dependent operations, and any code that mutates shared state. They run on the shared ForkJoinPool.commonPool().

Practice

  1. Easy: Change the size in ParallelSum to 1,000 and run it several times. Which is faster now, and why?
  2. Harder: Count the prime numbers below 2,000,000 with a sequential and a parallel IntStream, using a simple isPrime method, and compare the times.

Quick check: Parallel streams

4 questions
1Which thread pool do parallel streams use by default?
2Which source splits well for parallel work?
3What is wrong with parallel().forEach(list::add) on an ArrayList?
4When are parallel streams likely to be slower?

Key Takeaways

Was this tutorial helpful?

Lesson 323 of 482