mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-09-12 16:18:37 +00:00
* Add fork-join pattern implementation * Add input validation for start > end in SumTask Signed-off-by: SandhyaDevadiga <sandhyadevadiga8197@gmail.com> * Add fork-join module to parent pom.xml * Add missing assertThrows import in SumTaskTest * Format fork-join code with Spotless * Add AppTest to satisfy coverage requirements Signed-off-by: SandhyaDevadiga <sandhyadevadiga8197@gmail.com> --------- Signed-off-by: SandhyaDevadiga <sandhyadevadiga8197@gmail.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
---
|
||||
title: "Fork/Join Pattern in Java: Parallel Divide-and-Conquer Processing"
|
||||
shortTitle: Fork/Join
|
||||
description: "Learn the Fork/Join design pattern in Java with real-world examples, class diagrams, and code samples. Understand how to split large tasks into parallel subtasks for improved performance."
|
||||
category: Concurrency
|
||||
language: en
|
||||
tag:
|
||||
- Performance
|
||||
- Scalability
|
||||
- Concurrency
|
||||
---
|
||||
|
||||
## Also known as
|
||||
|
||||
* Divide and Conquer Parallelism
|
||||
* Work-Stealing Parallelism
|
||||
|
||||
## Intent of Fork/Join Design Pattern
|
||||
|
||||
The Fork/Join pattern recursively splits a large task into independent subtasks (fork),
|
||||
processes them in parallel across multiple threads, and combines their results (join) to
|
||||
produce a final outcome. It maximizes CPU utilization for computationally intensive problems.
|
||||
|
||||
## Detailed Explanation of Fork/Join Pattern with Real-World Examples
|
||||
|
||||
Real-world example
|
||||
|
||||
> Imagine a large warehouse that needs to count all its inventory items across 100 aisles.
|
||||
> Instead of one person counting every aisle sequentially, the manager divides the warehouse
|
||||
> into sections and assigns a team of workers to count each section simultaneously. Once
|
||||
> every section is counted, the manager collects all partial counts and sums them into the
|
||||
> total inventory. This is the Fork/Join pattern: split the work, do it in parallel, merge
|
||||
> the results.
|
||||
|
||||
In plain words
|
||||
|
||||
> Fork/Join splits a big problem into smaller pieces, solves each piece in parallel on
|
||||
> separate threads, then combines all results back together.
|
||||
|
||||
## Programmatic Example of Fork/Join Pattern in Java
|
||||
|
||||
We demonstrate the pattern by computing the sum of a large array in parallel using Java's
|
||||
built-in `ForkJoinPool` and `RecursiveTask`.
|
||||
|
||||
The `SumTask` is a recursive task that splits the array when it's too large:
|
||||
|
||||
```java
|
||||
public class SumTask extends RecursiveTask<Long> {
|
||||
|
||||
private static final int THRESHOLD = 1000;
|
||||
private final long[] numbers;
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
@Override
|
||||
protected Long compute() {
|
||||
int length = end - start;
|
||||
|
||||
if (length <= THRESHOLD) {
|
||||
// Base case: sum directly
|
||||
long sum = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
sum += numbers[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Fork: split into two halves
|
||||
int mid = start + length / 2;
|
||||
SumTask leftTask = new SumTask(numbers, start, mid);
|
||||
SumTask rightTask = new SumTask(numbers, mid, end);
|
||||
|
||||
leftTask.fork(); // run left half asynchronously
|
||||
long rightResult = rightTask.compute(); // compute right half here
|
||||
long leftResult = leftTask.join(); // wait for left half
|
||||
|
||||
// Join: combine results
|
||||
return leftResult + rightResult;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ForkJoinSumCalculator` provides a clean API:
|
||||
|
||||
```java
|
||||
public class ForkJoinSumCalculator {
|
||||
|
||||
private final ForkJoinPool pool;
|
||||
|
||||
public ForkJoinSumCalculator() {
|
||||
this.pool = ForkJoinPool.commonPool();
|
||||
}
|
||||
|
||||
public long calculateSum(long[] numbers) {
|
||||
SumTask task = new SumTask(numbers, 0, numbers.length);
|
||||
return pool.invoke(task);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Running the example in `App`:
|
||||
|
||||
```java
|
||||
long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray();
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
long result = calculator.calculateSum(numbers);
|
||||
System.out.println("Fork/Join sum: " + result);
|
||||
```
|
||||
|
||||
Program output:
|
||||
|
||||
```
|
||||
Fork/Join sum: 50000005000000
|
||||
Expected sum: 50000005000000
|
||||
Correct: true
|
||||
Time taken: 45 ms
|
||||
Available processors: 8
|
||||
```
|
||||
|
||||
## When to Use the Fork/Join Pattern in Java
|
||||
|
||||
* When you have a large, CPU-intensive task that can be divided into independent subtasks.
|
||||
* When the subtasks are roughly the same size and don't depend on each other.
|
||||
* When you want to utilize multiple CPU cores without manually managing threads.
|
||||
* When the problem naturally fits a divide-and-conquer strategy (e.g., sorting, searching,
|
||||
numerical computation).
|
||||
|
||||
## When NOT to Use Fork/Join
|
||||
|
||||
* For I/O-bound tasks (network calls, file reads) — use virtual threads or async I/O instead.
|
||||
* When subtasks are too small — the overhead of forking exceeds the benefit.
|
||||
* When tasks have dependencies on each other and cannot run independently.
|
||||
|
||||
## Benefits and Trade-offs of Fork/Join Pattern
|
||||
|
||||
Benefits:
|
||||
|
||||
* Maximizes CPU utilization through work-stealing algorithm.
|
||||
* Scales automatically with the number of available processors.
|
||||
* Built into Java's standard library (`java.util.concurrent`) — no external dependencies.
|
||||
* Clean recursive decomposition makes the code readable and maintainable.
|
||||
|
||||
Trade-offs:
|
||||
|
||||
* Overhead from task creation and thread management for very small problems.
|
||||
* Requires tasks to be independent — shared mutable state introduces bugs.
|
||||
* Choosing an appropriate threshold requires tuning for optimal performance.
|
||||
* Debugging parallel code is inherently harder than sequential code.
|
||||
|
||||
## Related Java Design Patterns
|
||||
|
||||
* [Divide and Conquer](https://java-design-patterns.com/patterns/divide-and-conquer/):
|
||||
Fork/Join is the parallel execution variant of the classic divide-and-conquer strategy.
|
||||
* [Thread Pool](https://java-design-patterns.com/patterns/thread-pool/): Fork/Join uses a
|
||||
specialized pool with work-stealing semantics.
|
||||
|
||||
## References
|
||||
|
||||
* [Java Documentation for ForkJoinPool](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ForkJoinPool.html)
|
||||
* [Java Concurrency in Practice — Brian Goetz](https://amzn.to/4aRMruW)
|
||||
* [Java Documentation for RecursiveTask](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/RecursiveTask.html)
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the terms of the MIT license, see LICENSE.md in the root
|
||||
of this project's GitHub repository for the full license text.
|
||||
|
||||
Copyright © 2014-2024 Ilkka Seppälä
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>fork-join</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.iluwatar.forkjoin.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
/**
|
||||
* The Fork/Join pattern is a concurrency design pattern that splits a large task into smaller
|
||||
* subtasks (fork), processes them in parallel, and then combines the results (join).
|
||||
*
|
||||
* <p>In Java, this pattern is implemented using {@link java.util.concurrent.ForkJoinPool} and
|
||||
* {@link java.util.concurrent.RecursiveTask}. Worker threads in the pool use a work-stealing
|
||||
* algorithm — idle threads take tasks from busy threads — maximizing CPU utilization.
|
||||
*
|
||||
* <p>In this example, we demonstrate the pattern by computing the sum of a large array in parallel.
|
||||
* The array is recursively split in half until each piece is small enough to sum directly, then
|
||||
* results are combined back up.
|
||||
*/
|
||||
public final class App {
|
||||
|
||||
private App() {}
|
||||
|
||||
/**
|
||||
* @param args command line arguments, not used
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// Create an array of 10 million numbers: [1, 2, 3, ..., 10_000_000]
|
||||
long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray();
|
||||
|
||||
// Calculate sum using Fork/Join
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
long startTime = System.currentTimeMillis();
|
||||
long result = calculator.calculateSum(numbers);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
// The expected sum of 1 to N is N*(N+1)/2
|
||||
long expected = 10_000_000L * 10_000_001L / 2;
|
||||
|
||||
System.out.println("Fork/Join sum: " + result);
|
||||
System.out.println("Expected sum: " + expected);
|
||||
System.out.println("Correct: " + (result == expected));
|
||||
System.out.println("Time taken: " + (endTime - startTime) + " ms");
|
||||
System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
/**
|
||||
* ForkJoinSumCalculator provides a convenient API to sum an array of numbers using the Fork/Join
|
||||
* framework. It creates a {@link ForkJoinPool}, submits a {@link SumTask}, and returns the computed
|
||||
* sum.
|
||||
*
|
||||
* <p>The pool manages a set of worker threads that process subtasks in parallel. Idle threads can
|
||||
* "steal" work from busy threads, maximizing CPU utilization.
|
||||
*/
|
||||
public class ForkJoinSumCalculator {
|
||||
|
||||
private final ForkJoinPool pool;
|
||||
|
||||
/** Creates a calculator using the common ForkJoinPool (uses all available CPU cores). */
|
||||
public ForkJoinSumCalculator() {
|
||||
this.pool = ForkJoinPool.commonPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a calculator with a specific number of threads.
|
||||
*
|
||||
* @param parallelism the number of worker threads to use
|
||||
*/
|
||||
public ForkJoinSumCalculator(int parallelism) {
|
||||
this.pool = new ForkJoinPool(parallelism);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of all elements in the array using Fork/Join parallelism.
|
||||
*
|
||||
* @param numbers the array of numbers to sum
|
||||
* @return the total sum of all elements
|
||||
*/
|
||||
public long calculateSum(long[] numbers) {
|
||||
if (numbers == null || numbers.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
SumTask task = new SumTask(numbers, 0, numbers.length);
|
||||
return pool.invoke(task);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import java.util.concurrent.RecursiveTask;
|
||||
|
||||
/**
|
||||
* SumTask demonstrates the Fork/Join pattern by recursively splitting an array summation problem
|
||||
* into smaller subtasks until each subtask is small enough to compute directly.
|
||||
*
|
||||
* <p>How it works: If the portion of the array is smaller than THRESHOLD, sum it in a simple loop.
|
||||
* Otherwise, split the array in half, fork one half to run in parallel, compute the other half in
|
||||
* the current thread, and then join the results. This approach utilizes multiple CPU cores to
|
||||
* perform the summation significantly faster than a single-threaded loop for large arrays.
|
||||
*/
|
||||
public class SumTask extends RecursiveTask<Long> {
|
||||
|
||||
/**
|
||||
* If the number of elements to process is at or below this threshold, the task computes the sum
|
||||
* directly instead of splitting further.
|
||||
*/
|
||||
private static final int THRESHOLD = 1000;
|
||||
|
||||
private final long[] numbers;
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
/**
|
||||
* Creates a task to sum elements of the given array from index {@code start} (inclusive) to index
|
||||
* {@code end} (exclusive).
|
||||
*
|
||||
* @param numbers the array of numbers to sum
|
||||
* @param start the starting index (inclusive)
|
||||
* @param end the ending index (exclusive)
|
||||
*/
|
||||
public SumTask(long[] numbers, int start, int end) {
|
||||
if (start > end) {
|
||||
throw new IllegalArgumentException(
|
||||
"start (" + start + ") must not be greater than end (" + end + ")");
|
||||
}
|
||||
this.numbers = numbers;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main computation method. This is where the fork/join magic happens.
|
||||
*
|
||||
* @return the sum of elements from start to end
|
||||
*/
|
||||
@Override
|
||||
protected Long compute() {
|
||||
int length = end - start;
|
||||
|
||||
// BASE CASE: if the chunk is small enough, just sum directly
|
||||
if (length <= THRESHOLD) {
|
||||
return computeDirectly();
|
||||
}
|
||||
|
||||
// FORK: split the task into two halves
|
||||
int mid = start + length / 2;
|
||||
|
||||
// Create subtask for the left half
|
||||
SumTask leftTask = new SumTask(numbers, start, mid);
|
||||
|
||||
// Create subtask for the right half
|
||||
SumTask rightTask = new SumTask(numbers, mid, end);
|
||||
|
||||
// Fork the left task — it will run in a separate thread
|
||||
leftTask.fork();
|
||||
|
||||
// Compute the right task in the current thread (no need to fork both)
|
||||
long rightResult = rightTask.compute();
|
||||
|
||||
// JOIN: wait for the left task to finish and get its result
|
||||
long leftResult = leftTask.join();
|
||||
|
||||
// Combine the results from both halves
|
||||
return leftResult + rightResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the sum directly using a simple loop. This is used when the chunk size is at or below
|
||||
* the threshold — no further splitting needed.
|
||||
*
|
||||
* @return the sum of elements in the range [start, end)
|
||||
*/
|
||||
private long computeDirectly() {
|
||||
long sum = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
sum += numbers[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AppTest {
|
||||
|
||||
@Test
|
||||
void shouldExecuteWithoutException() {
|
||||
App.main(new String[] {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.stream.LongStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ForkJoinSumCalculatorTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnZeroForNullArray() {
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
|
||||
assertEquals(0L, calculator.calculateSum(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnZeroForEmptyArray() {
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
|
||||
assertEquals(0L, calculator.calculateSum(new long[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCalculateSumOfSmallArray() {
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
long[] numbers = {10, 20, 30, 40, 50};
|
||||
|
||||
long result = calculator.calculateSum(numbers);
|
||||
|
||||
assertEquals(150L, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCalculateSumOfLargeArray() {
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator();
|
||||
long[] numbers = LongStream.rangeClosed(1, 100_000).toArray();
|
||||
|
||||
long result = calculator.calculateSum(numbers);
|
||||
|
||||
long expected = 100_000L * 100_001L / 2;
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithCustomParallelism() {
|
||||
// Use only 2 threads
|
||||
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(2);
|
||||
long[] numbers = LongStream.rangeClosed(1, 50_000).toArray();
|
||||
|
||||
long result = calculator.calculateSum(numbers);
|
||||
|
||||
long expected = 50_000L * 50_001L / 2;
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.iluwatar.forkjoin;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.stream.LongStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SumTaskTest {
|
||||
|
||||
@Test
|
||||
void shouldSumSmallArrayDirectly() {
|
||||
// Array smaller than threshold — should compute without forking
|
||||
long[] numbers = {1, 2, 3, 4, 5};
|
||||
SumTask task = new SumTask(numbers, 0, numbers.length);
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
assertEquals(15L, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSumLargeArrayUsingForkJoin() {
|
||||
// Array larger than threshold — will fork into subtasks
|
||||
long[] numbers = LongStream.rangeClosed(1, 10_000).toArray();
|
||||
SumTask task = new SumTask(numbers, 0, numbers.length);
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
// Sum of 1 to N = N*(N+1)/2
|
||||
long expected = 10_000L * 10_001L / 2;
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSumPartialRange() {
|
||||
// Sum only a portion of the array (indices 2 to 5)
|
||||
long[] numbers = {10, 20, 30, 40, 50, 60};
|
||||
SumTask task = new SumTask(numbers, 2, 5);
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
// 30 + 40 + 50 = 120
|
||||
assertEquals(120L, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnZeroForEmptyRange() {
|
||||
long[] numbers = {1, 2, 3};
|
||||
SumTask task = new SumTask(numbers, 1, 1); // start == end, empty range
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
assertEquals(0L, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSingleElement() {
|
||||
long[] numbers = {42};
|
||||
SumTask task = new SumTask(numbers, 0, 1);
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
assertEquals(42L, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceCorrectResultForMillionElements() {
|
||||
long[] numbers = LongStream.rangeClosed(1, 1_000_000).toArray();
|
||||
SumTask task = new SumTask(numbers, 0, numbers.length);
|
||||
|
||||
long result = ForkJoinPool.commonPool().invoke(task);
|
||||
|
||||
long expected = 1_000_000L * 1_000_001L / 2;
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenStartGreaterThanEnd() {
|
||||
long[] numbers = {1, 2, 3, 4, 5};
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> new SumTask(numbers, 4, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user