JAVA

Java Parallel Stream and Fork/Join Pattern

neal89 2025. 4. 4. 13:50

Here is a complete English summary for your Tistory blog post about Java Parallel Streams, including a refined Fork/Join example, best practices, and notes on CompletableFuture.


Java Parallel Stream and Fork/Join Framework

Parallel programming in Java allows developers to leverage multi-core CPUs efficiently. In this post, we explore how Java handles parallel processing, from basic multithreading to the Fork/Join framework and parallel streams. We'll also touch on pitfalls and practical guidelines for using these features effectively.


1. Sequential Stream

A simple stream processes elements one by one on a single thread, usually the main thread.

int sum = IntStream.rangeClosed(1, 8)
    .map(HeavyJob::heavyTask)
    .sum();

This example simulates heavy computation per element, taking about 8 seconds in total.


2. Manual Multithreading

We can divide tasks manually using Thread and Runnable:

class SumWorker implements Runnable {
    private final int start, end;
    public int result;

    public SumWorker(int start, int end) {
        this.start = start;
        this.end = end;
    }

    public void run() {
        for (int i = start; i <= end; i++) {
            result += HeavyJob.heavyTask(i);
        }
    }
}

Manually splitting workload and joining threads reduces execution time but introduces complexity.


3. Thread Pool with ExecutorService

Use ExecutorService and Callable for easier thread management and result aggregation:

ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f1 = pool.submit(() -> computeSum(1, 4));
Future<Integer> f2 = pool.submit(() -> computeSum(5, 8));

int total = f1.get() + f2.get();

This simplifies threading logic and scales better.


4. Fork/Join Pattern and RecursiveTask

The Fork/Join pattern splits a task recursively and merges results.

Example

public class ParallelSumTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 4;
    private final List<Integer> numbers;

    public ParallelSumTask(List<Integer> numbers) {
        this.numbers = numbers;
    }

    @Override
    protected Integer compute() {
        if (numbers.size() <= THRESHOLD) {
            MyLogger.log("Direct: " + numbers);
            return numbers.stream().mapToInt(HeavyJob::heavyTask).sum();
        }

        int mid = numbers.size() / 2;
        ParallelSumTask left = new ParallelSumTask(numbers.subList(0, mid));
        ParallelSumTask right = new ParallelSumTask(numbers.subList(mid, numbers.size()));

        left.fork();
        int rightResult = right.compute();
        int leftResult = left.join();
        return leftResult + rightResult;
    }
}

This recursive approach simplifies parallel processing and load distribution.


5. ForkJoinPool and Common Pool

You can execute tasks with a custom or common pool:

ForkJoinPool pool = new ForkJoinPool(); // custom pool
int result = pool.invoke(new ParallelSumTask(data));

Or just use:

int result = new ParallelSumTask(data).invoke(); // uses common pool

⚠️ If no pool is explicitly provided, Java uses the common pool by default.


6. Java Parallel Stream

With .parallel(), Java streams use the Fork/Join common pool behind the scenes.

int sum = IntStream.rangeClosed(1, 8)
    .parallel()
    .map(HeavyJob::heavyTask)
    .sum();

This approach requires no manual thread handling. However, because it uses the shared pool, resource contention can become a serious problem in high-concurrency environments.


7. When (Not) to Use Parallel Stream

✅ Good for:

  • CPU-bound, compute-heavy operations
  • Short-lived, isolated computations

❌ Avoid for:

  • I/O-bound operations (e.g., API calls, DB access)
  • Web servers handling multiple parallel requests

Why? ForkJoin common pool has a limited number of threads (usually CPU count - 1). If threads are blocked by I/O, they can’t help other tasks.


8. CompletableFuture and Fork/Join Pool

When using CompletableFuture.runAsync() or supplyAsync(), if you don't provide a custom executor, it will also use the Fork/Join common pool by default:

// Uses common pool
CompletableFuture.runAsync(() -> MyLogger.log("common pool"));

// Uses custom pool
ExecutorService customPool = Executors.newFixedThreadPool(10);
CompletableFuture.runAsync(() -> MyLogger.log("custom"), customPool);

To avoid blocking the shared pool, always use a custom pool for I/O-bound tasks with CompletableFuture.


Summary

    

Feature Suitable For Custom Thread Pool Returns Result Notes
Stream Simple, linear logic Easy to use
Parallel Stream CPU-bound tasks ❌ (common pool) Risk of contention
Thread Fine control needed ✅ (manual) High complexity
ExecutorService Scalable tasks ✅ (Future) Preferred for most
ForkJoinTask Recursive processing ✅ / common Best for divide-and-conquer
CompletableFuture Async pipelines ✅ recommended Use custom pool!

Let me know if you want this in Markdown or HTML format, or if you'd like to add a diagram for the Fork/Join flow.

'JAVA' 카테고리의 다른 글

Exception  (0) 2025.04.16
Default Methods in Java  (0) 2025.04.04
Java Optional  (0) 2025.04.04
Java Method Reference  (0) 2025.04.02
Java Functional Interfaces  (0) 2025.04.01