Skip to main content

14.5 Parallel Streams and Primitive Streams

1. Parallel Streams

parallelStream() processes stream operations in parallel using multiple CPU cores via the internal ForkJoinPool.

import java.util.*;
import java.util.stream.*;

List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed().collect(Collectors.toList());

// Sequential stream
long start1 = System.currentTimeMillis();
long sum1 = numbers.stream()
.mapToLong(Integer::longValue).sum();
System.out.println("Sequential: " + (System.currentTimeMillis() - start1) + "ms, sum=" + sum1);

// Parallel stream
long start2 = System.currentTimeMillis();
long sum2 = numbers.parallelStream()
.mapToLong(Integer::longValue).sum();
System.out.println("Parallel: " + (System.currentTimeMillis() - start2) + "ms, sum=" + sum2);

When to Use Parallel Streams

SuitableNot suitable
Very large data sets (tens of thousands or more)Small data sets
CPU-intensive operationsSimple, fast operations
Independent operations (order doesn't matter)Order-sensitive processing
Data structures easy to split, like ArrayListData structures hard to split, like LinkedList
// Suitable for parallel: complex operations, large data
List<Integer> bigList = IntStream.rangeClosed(1, 100_000)
.boxed().collect(Collectors.toList());

long result = bigList.parallelStream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> (long) n * n)
.sum();

// Not suitable for parallel: order-sensitive, I/O-bound
List<String> files = List.of("a.txt", "b.txt", "c.txt");
// File I/O operations may not benefit from parallelism and can break ordering

Warning: Never Share Mutable State

// Dangerous! Modifying shared state
List<Integer> shared = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(shared::add); // ConcurrentModificationException!

// Safe: use collect instead
List<Integer> safe = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList()); // thread-safe internally

2. Primitive Streams

Generic Stream<Integer> has overhead due to object boxing. IntStream, LongStream, and DoubleStream work directly with primitive types for much better performance.

IntStream

// Creation methods
IntStream s1 = IntStream.of(1, 2, 3, 4, 5);
IntStream s2 = IntStream.range(1, 6); // 1,2,3,4,5 (exclusive end)
IntStream s3 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5 (inclusive end)
IntStream s4 = "Hello".chars(); // int code of each character

// Statistical operations (no boxing overhead)
int sum = IntStream.rangeClosed(1, 100).sum(); // 5050
double avg = IntStream.rangeClosed(1, 100).average().orElse(0); // 50.5
int max = IntStream.of(3, 1, 4, 1, 5, 9).max().orElse(0); // 9
long count = IntStream.rangeClosed(1, 100).count(); // 100

// Summary statistics in one call
IntSummaryStatistics stats = IntStream.rangeClosed(1, 100).summaryStatistics();
System.out.println(stats.getSum()); // 5050
System.out.println(stats.getAverage()); // 50.5
System.out.println(stats.getMax()); // 100

// Convert to array
int[] arr = IntStream.rangeClosed(1, 5).toArray(); // [1, 2, 3, 4, 5]

// Boxing (convert to Stream<Integer>)
Stream<Integer> boxed = IntStream.rangeClosed(1, 5).boxed();
List<Integer> list = boxed.collect(Collectors.toList());

LongStream and DoubleStream

// LongStream: handling large numbers
long factorial = LongStream.rangeClosed(1, 20)
.reduce(1L, (a, b) -> a * b);
System.out.println(factorial); // 2432902008176640000

// DoubleStream: floating-point operations
double[] prices = {100.0, 200.0, 300.0, 150.0};
DoubleSummaryStatistics priceStats = DoubleStream.of(prices).summaryStatistics();
System.out.printf("Average price: %.2f%n", priceStats.getAverage()); // 187.50

mapToInt, mapToLong, mapToDouble — Converting to Primitive Streams

record Product(String name, int price, int quantity) {}

List<Product> products = List.of(
new Product("Apple", 1000, 50),
new Product("Banana", 800, 30),
new Product("Strawberry", 2000, 20)
);

// Total inventory value (no int boxing overhead)
int totalValue = products.stream()
.mapToInt(p -> p.price() * p.quantity())
.sum();
System.out.println("Total inventory value: " + totalValue); // 126000

// Most expensive product
OptionalInt maxPrice = products.stream()
.mapToInt(Product::price)
.max();
System.out.println("Max price: " + maxPrice.orElse(0)); // 2000

// Average price
OptionalDouble avgPrice = products.stream()
.mapToDouble(Product::price)
.average();
System.out.printf("Average price: %.0f%n", avgPrice.orElse(0)); // 1267

3. Advanced Stream Creation Methods

// Stream.of
Stream<String> s1 = Stream.of("A", "B", "C");

// Stream.generate - infinite stream (always use limit!)
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
Stream<String> helloStream = Stream.generate(() -> "hello").limit(3);

// Stream.iterate - generate from initial value + function
Stream<Integer> evens = Stream.iterate(0, n -> n + 2).limit(10);
// 0, 2, 4, 6, 8, 10, 12, 14, 16, 18

// Stream.iterate with condition (Java 9+)
Stream<Integer> under100 = Stream.iterate(1, n -> n < 100, n -> n * 2);
// 1, 2, 4, 8, 16, 32, 64

// Stream.concat - merge two streams
Stream<String> combined = Stream.concat(
Stream.of("A", "B"),
Stream.of("C", "D")
);
combined.forEach(System.out::print); // ABCD

// Stream.builder
Stream.Builder<String> builder = Stream.builder();
builder.add("A");
builder.add("B");
if (true) builder.add("C");
Stream<String> built = builder.build();

Pro Tips

Practical guide to parallel streams:

  1. Measure first: Parallel streams are not always faster. For small data or simple operations, they can actually be slower. Measure with System.nanoTime() before adopting.

  2. ForkJoinPool thread count: By default, it uses Runtime.getRuntime().availableProcessors() - 1 threads. Indiscriminate use of parallelStream() in a Spring web server can exhaust the shared thread pool.

  3. Prefer primitive streams: Use IntStream, LongStream, etc. instead of Stream<Integer>, Stream<Long> to eliminate boxing overhead. The difference is especially significant for numeric aggregations.

// Slow: boxing overhead
long sum1 = Stream.iterate(1, n -> n + 1).limit(1_000_000)
.mapToLong(Integer::longValue).sum();

// Fast: use primitive stream from the start
long sum2 = LongStream.rangeClosed(1, 1_000_000).sum();