Java 8 – Parallel Streams Example

Introduction

Java 8 introduced the Stream API, which provides a powerful and flexible way to process collections of data. One of the most significant features of the Stream API is the ability to perform parallel processing using parallel streams. Parallel streams allow you to divide a stream’s elements into multiple substreams and process them in parallel, leveraging multiple CPU cores for better performance. This can lead to significant performance improvements in cases where large datasets need to be processed.

In this guide, we’ll explore how to use parallel streams in Java 8, along with examples demonstrating their use and potential performance benefits.

Table of Contents

  • Problem Statement
  • Solution Steps
  • Java Program
    • Example 1: Basic Parallel Stream Example
    • Example 2: Comparing Sequential and Parallel Streams
    • Example 3: Handling Thread Safety in Parallel Streams
    • Example 4: Parallel Stream with Complex Data Processing
    • Example 5: Performance Considerations and Best Practices
  • Conclusion

Problem Statement

When working with large datasets or computationally intensive tasks, sequential processing may become a bottleneck. The goal is to use parallel streams to speed up the processing of data by utilizing multiple CPU cores effectively.

Example:

  • Problem: Sequentially processing a large dataset can be slow and inefficient.
  • Goal: Use parallel streams to improve the performance of data processing tasks by parallelizing the workload across multiple threads.

Solution Steps

  1. Convert to Parallel Stream: Learn how to convert a sequential stream to a parallel stream.
  2. Compare Performance: Compare the performance of sequential and parallel streams.
  3. Ensure Thread Safety: Handle thread safety concerns when using parallel streams.
  4. Process Complex Data: Apply parallel streams to more complex data processing tasks.
  5. Consider Performance: Understand the best practices for using parallel streams effectively.

Java Program

Example 1: Basic Parallel Stream Example

The simplest way to create a parallel stream is by calling the parallelStream() method on a collection. Here’s a basic example:

import java.util.Arrays;
import java.util.List;

/**
 * Java 8 - Basic Parallel Stream Example
 * Author: https://www.rameshfadatare.com/
 */
public class ParallelStreamExample1 {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Amit", "Priya", "Raj", "Suman", "Kiran");

        // Process elements using a parallel stream
        names.parallelStream()
             .forEach(name -> System.out.println("Processing " + name + " in thread " + Thread.currentThread().getName()));
    }
}

Output (example, actual output may vary)

Processing Priya in thread ForkJoinPool.commonPool-worker-3
Processing Amit in thread ForkJoinPool.commonPool-worker-5
Processing Raj in thread main
Processing Suman in thread ForkJoinPool.commonPool-worker-7
Processing Kiran in thread ForkJoinPool.commonPool-worker-5

Explanation

  • parallelStream(): Converts the list to a parallel stream, enabling parallel processing.
  • Parallel Processing: Elements are processed in parallel by different threads from the ForkJoinPool, which is the default thread pool used by parallel streams.

Example 2: Comparing Sequential and Parallel Streams

Let’s compare the performance of sequential and parallel streams when processing a large dataset.

import java.util.ArrayList;
import java.util.List;

/**
 * Java 8 - Comparing Sequential and Parallel Streams
 * Author: https://www.rameshfadatare.com/
 */
public class ParallelStreamExample2 {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 10_000_000; i++) {
            numbers.add(i);
        }

        // Sequential stream processing
        long startTime = System.currentTimeMillis();
        long sequentialSum = numbers.stream()
                                    .reduce(0, Integer::sum);
        long endTime = System.currentTimeMillis();
        System.out.println("Sequential sum: " + sequentialSum + " in " + (endTime - startTime) + " ms");

        // Parallel stream processing
        startTime = System.currentTimeMillis();
        long parallelSum = numbers.parallelStream()
                                  .reduce(0, Integer::sum);
        endTime = System.currentTimeMillis();
        System.out.println("Parallel sum: " + parallelSum + " in " + (endTime - startTime) + " ms");
    }
}

Output (example, actual output may vary)

Sequential sum: 50000005000000 in 85 ms
Parallel sum: 50000005000000 in 23 ms

Explanation

  • Sequential Stream: The numbers are processed sequentially, which may be slower for large datasets.
  • Parallel Stream: The numbers are processed in parallel, which significantly reduces the processing time.

Example 3: Handling Thread Safety in Parallel Streams

When using parallel streams, it’s essential to ensure that operations on shared resources are thread-safe.

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Java 8 - Thread Safety in Parallel Streams
 * Author: https://www.rameshfadatare.com/
 */
public class ParallelStreamExample3 {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Amit", "Priya", "Raj", "Suman", "Kiran");

        // Thread-safe counter using AtomicInteger
        AtomicInteger counter = new AtomicInteger(0);

        // Process elements using a parallel stream and ensure thread safety
        names.parallelStream().forEach(name -> {
            int count = counter.incrementAndGet();
            System.out.println("Processing " + name + ": Count " + count + " in thread " + Thread.currentThread().getName());
        });

        System.out.println("Final count: " + counter.get());
    }
}

Output (example, actual output may vary)

Processing Priya: Count 1 in thread ForkJoinPool.commonPool-worker-7
Processing Amit: Count 2 in thread ForkJoinPool.commonPool-worker-5
Processing Raj: Count 3 in thread main
Processing Suman: Count 4 in thread ForkJoinPool.commonPool-worker-5
Processing Kiran: Count 5 in thread ForkJoinPool.commonPool-worker-7
Final count: 5

Explanation

  • Thread Safety: AtomicInteger is used to ensure that increments are thread-safe when processing elements in parallel.

Example 4: Parallel Stream with Complex Data Processing

Parallel streams can be beneficial when processing complex data or performing computationally intensive tasks.

import java.util.Arrays;
import java.util.List;

/**
 * Java 8 - Parallel Stream with Complex Data Processing
 * Author: https://www.rameshfadatare.com/
 */
public class ParallelStreamExample4 {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Amit", "Priya", "Raj", "Suman", "Kiran");

        // Simulate complex processing with parallel stream
        names.parallelStream().forEach(name -> {
            try {
                Thread.sleep(100); // Simulate a time-consuming task
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Processed " + name + " in thread " + Thread.currentThread().getName());
        });
    }
}

Output (example, actual output may vary)

Processed Priya in thread ForkJoinPool.commonPool-worker-7
Processed Amit in thread ForkJoinPool.commonPool-worker-5
Processed Raj in thread main
Processed Suman in thread ForkJoinPool.commonPool-worker-7
Processed Kiran in thread ForkJoinPool.commonPool-worker-5

Explanation

  • Simulated Processing: Each name is processed in parallel, simulating a time-consuming task using Thread.sleep() to represent complex data processing.

Example 5: Performance Considerations and Best Practices

Parallel streams can offer significant performance benefits, but they are not always the best choice. Here are some considerations:

  • Data Size: Parallel streams are most effective with large datasets. For small datasets, the overhead of parallelization may outweigh the performance gains.
  • Thread Safety: Ensure that any shared resources are thread-safe when using parallel streams.
  • Order of Operations: Be cautious of operations that depend on the order of elements. In such cases, use forEachOrdered() instead of forEach().

Conclusion

Parallel streams in Java 8 offer a powerful way to process large datasets and computationally intensive tasks more efficiently by leveraging multiple CPU cores. By understanding how to use parallel streams and ensuring thread safety, you can significantly improve the performance of your Java applications. However, it’s essential to consider the trade-offs and best practices to determine when parallel streams are appropriate for your use case.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top