diff --git a/fork-join/README.md b/fork-join/README.md new file mode 100644 index 000000000000..b94af1de02ac --- /dev/null +++ b/fork-join/README.md @@ -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 { + + 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) diff --git a/fork-join/pom.xml b/fork-join/pom.xml new file mode 100644 index 000000000000..23958727728a --- /dev/null +++ b/fork-join/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + fork-join + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.forkjoin.App + + + + + + + + + + diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/App.java b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java new file mode 100644 index 000000000000..b1d068b8ec86 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java @@ -0,0 +1,45 @@ +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). + * + * 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. + * + * 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()); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java new file mode 100644 index 000000000000..19544ae1f189 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java @@ -0,0 +1,47 @@ +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. + * + * 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); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java new file mode 100644 index 000000000000..3fcaf9a84f1a --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java @@ -0,0 +1,95 @@ +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. + * + * 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 { + + /** + * 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; + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java new file mode 100644 index 000000000000..6af56dfc38e1 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java @@ -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); + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java new file mode 100644 index 000000000000..f52eea068a82 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java @@ -0,0 +1,84 @@ +package com.iluwatar.forkjoin; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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)); + } +}