-
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Add Fork/Join design pattern (#3227) #3550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
SandhyaDevadiga
wants to merge
6
commits into
iluwatar:master
Choose a base branch
from
SandhyaDevadiga:fork-join-pattern-3227
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0f14977
Add fork-join pattern implementation
SandhyaDevadiga f5fe108
Add input validation for start > end in SumTask
SandhyaDevadiga 0a403c3
Add fork-join module to parent pom.xml
SandhyaDevadiga 4a9ef8f
Add missing assertThrows import in SumTaskTest
SandhyaDevadiga f96697f
Format fork-join code with Spotless
SandhyaDevadiga 2a43036
Add AppTest to satisfy coverage requirements
SandhyaDevadiga File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()); | ||
| } | ||
| } |
44 changes: 44 additions & 0 deletions
44
fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
93 changes: 93 additions & 0 deletions
93
fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.