Sitemap

Java : Parallel Streams

A Comprehensive Guide to Parallel Streams in Java

Java introduced the concept of Streams in Java 8 to provide a more functional and declarative approach to processing data. While traditional loops and collections can be used to manipulate and transform data, Java Streams offer a cleaner, more readable, and more efficient way to handle collections. However, Streams are not limited to sequential execution; Java also introduced Parallel Streams to make data processing faster by leveraging multicore processors.

In this blog, we’ll dive deep into Parallel Streams in Java — what they are, how they work, and when to use them effectively.

What are Parallel Streams?

A Parallel Stream is simply a Stream that runs operations concurrently on multiple threads. This allows tasks to be executed in parallel, potentially leading to significant performance improvements when processing large datasets or computationally expensive operations.

In a sequential stream, each operation is processed one by one. In contrast, a parallel stream divides the data into chunks and processes them concurrently, utilizing the underlying hardware’s multi-core processor.

Sequential vs Parallel Streams

Before we dive deeper into parallel streams, it’s important to understand the difference between Sequential Streams and Parallel Streams:

Sequential Streams:

  • Process elements one at a time.
  • Operate on a single thread.
  • Execution order is predictable and deterministic.
  • Example: list.stream()

Parallel Streams:

  • Process elements in parallel across multiple threads.
  • Split the data into smaller chunks and process them concurrently.
  • Execution order is not guaranteed (this is an important consideration for stateful operations).
  • Example: list.parallelStream()

How Do Parallel Streams Work?

Under the hood, Parallel Streams utilize the ForkJoinPool to manage parallelism. The data is split into chunks, which are processed by separate threads concurrently. After all the chunks are processed, the results are combined to form the final result.

Get Rupam Thakre’s stories in your inbox

Join Medium for free to get updates from this writer.

When you call parallelStream() on a collection, the following things happen:

  1. The data is divided into smaller, independent chunks (partitions).
  2. Each partition is processed by a different thread, typically from the ForkJoinPool.
  3. After all partitions are processed, the results are combined using a reduction operation (e.g., sum, min, max).

Java uses a thread pool, known as the ForkJoinPool.commonPool(), by default for parallel stream processing. You can also customize the pool if necessary.

How to Use Parallel Streams in Java

Using parallel streams in Java is straightforward. The major difference between a sequential stream and a parallel stream is the method call. You switch from a sequential stream to a parallel stream by calling parallelStream() instead of stream().

Example: Sequential Stream

import java.util.List;
import java.util.Arrays;
public class SequentialStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();

System.out.println("Sum of even numbers: " + sum);
}
}

Example: Parallel Streams

import java.util.List;
import java.util.Arrays;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

int sum = numbers.parallelStream() // Use parallelStream instead of stream
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();

System.out.println("Sum of even numbers (parallel): " + sum);
}
}

Key Advantages of Parallel Streams

  1. Improved Performance on Large Datasets:
    By splitting the workload into smaller chunks and processing them concurrently, parallel streams can significantly improve performance for large datasets or CPU-intensive tasks.
  2. Simplicity:
    Parallel streams make it easy to parallelize data processing without dealing with low-level thread management. You simply switch from stream() to parallelStream().
  3. Utilization of Multi-Core Processors:
    Java automatically leverages multi-core processors, meaning parallel streams take full advantage of your machine’s available CPU cores.
  4. Cleaner Code:
    Parallel streams help make code more readable by abstracting away the complexity of multi-threading and concurrency. You don’t need to manually manage thread creation and synchronization.

Considerations When Using Parallel Streams

While parallel streams are powerful, they are not always the best choice. Here are some key considerations to keep in mind:

  1. Stateful Operations: Parallel streams do not guarantee execution order, so stateful operations (operations that rely on the order of elements or previous results, such as findFirst() or reduce()) may yield unexpected or incorrect results in parallel processing. For example, calling findFirst() on a parallel stream may return an element from any of the partitions rather than the first element of the stream.
  2. Performance Overhead: Not all tasks benefit from parallelism. For small datasets or lightweight operations, the overhead of managing threads and synchronizing results may outweigh the benefits of parallelism. In these cases, sequential streams are more efficient.
  3. Thread Contention: Parallel streams are subject to thread contention, where multiple threads may attempt to access shared resources concurrently. This can lead to issues like race conditions, deadlocks, or other synchronization problems if not handled correctly.
  4. Limited Parallelism for IO Operations: Parallel streams are most effective for CPU-bound tasks. For IO-bound tasks (e.g., reading from files, accessing databases, etc.), the benefits of parallelism are often limited because IO operations typically do not utilize CPU resources efficiently.

When to Use Parallel Streams

Here are some scenarios where parallel streams are a good choice:

  • CPU-Intensive Tasks: Parallel streams are ideal when you’re working with large datasets that require heavy computations (e.g., mathematical calculations, complex transformations, etc.).
  • Data Processing on Collections: If you’re working with large collections of data that can be partitioned into independent chunks, parallel streams can speed up processing significantly.
  • Transformations and Aggregations: If you need to perform transformations (e.g., map()) or reductions (e.g., sum(), reduce()) on collections, parallel streams can often perform these operations faster.

When Not to Use Parallel Streams

Parallel streams may not be suitable in the following cases:

  • Small Datasets: For small collections, parallel streams often add unnecessary overhead, and a sequential stream will perform just as well.
  • Stateful Operations: Avoid parallel streams if you rely on operations where the order of execution matters, or where the state of the operation is modified.
  • I/O-Bound Tasks: When performing I/O operations, parallel streams won’t significantly improve performance since they don’t optimize I/O tasks like they do for CPU-bound operations.

Conclusion

Parallel Streams in Java offer a powerful mechanism for parallelizing operations on large datasets, making it easier to take advantage of multicore processors. However, like any concurrency-related tool, they should be used with care. Always consider the nature of the task — whether it’s CPU-bound or I/O-bound — and the characteristics of the operations you are performing. When used in the right context, parallel streams can lead to significant performance improvements and cleaner, more maintainable code.

Written by Rupam Thakre

Java Developer, SpringBoot Developer, Backend Development, Microservices Rest API Development, Angular, Apache Camel, Apache Kafka