Difference Between Fork/Join Framework and ExecutorService in Java


In Java's concurrent programming domain lies a plethora of choices for developers to choose from. The Fork/Join Framework and ExecutorService present two of these alternatives that stand out by popularity. Although both solutions excel at parallelizing operations reasonably well, they differ in how they are structured for use cases' varying requirements. Through this writing piece's insight on each framework's syntax properties paired with practical coding examples users can gain a better understanding of what makes each standout when compared together.

Syntax

Fork/Join Framework

class ForkJoinTask<V> extends Object

ExecutorService

interface ExecutorService extends Executor

Explanation of Syntax

The Fork/Join Framework is built around the ForkJoinTask class, which represents a task that can be divided into smaller subtasks. Enrolling in this program offers you an opportunity to learn about recursive decomposition of tasks and how to execute them concurrently. Moreover, with the use of the ExecutorService interface that builds ontopoftheExecutorinterface,you'll be abletocarryoutasynchronous task execution in a superior way. It manages a pool of threads and handles the submission and execution of tasks.

Approach 1: Fork/Join Framework

Algorithm

  • Define a ForkJoinTask subclass that represents the task to be performed.

  • Implement the compute() method in the subclass, dividing the task into smaller subtasks and invoking their execution.

  • Combine the results from subtasks to produce the final result.

Full Ready-to-Execute Code for Approach 1

Example

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class MyTask extends RecursiveTask {
   private static final int THRESHOLD = 10;

   private int[] array;
   private int start;
   private int end;

   public MyTask(int[] array, int start, int end) {
      this.array = array;
      this.start = start;
      this.end = end;
   }

   @Override
   protected Integer compute() {
      if (end - start <= THRESHOLD) {
         // Perform the computation directly
         int sum = 0;
         for (int i = start; i < end; i++) {
            sum += array[i];
         }
         return sum;
      } else {
         // Divide the task into smaller subtasks
         int mid = start + (end - start) / 2;
         MyTask leftTask = new MyTask(array, start, mid);
         MyTask rightTask = new MyTask(array, mid, end);

         // Fork the subtasks
         leftTask.fork();
         rightTask.fork();

         // Combine the results
         int leftResult = leftTask.join();
         int rightResult = rightTask.join();

         // Return the final result
         return leftResult + rightResult;
      }
   }
}

public class ForkJoinExample {
   public static void main(String[] args) {
      int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

      MyTask task = new MyTask(array, 0, array.length);

      // Create a ForkJoinPool and invoke the task
      ForkJoinPool pool = new ForkJoinPool();
      int result = pool.invoke(task);

      System.out.println("Result: " + result);
   }
}

Output

Result: 55

Explanation of the Code in Approach 1

Our approach entails creating a specialized category known as MyTask which derives from RecursiveTask<Integer> so we can run calculations and receive output. To accomplish this objective, we modify the compute() method to partition bigger tasks into minor ones whenever they exceed our set limit. The smaller subtasks are then forked and their results are joined together to produce the final result.

Approach 2: ExecutorService

Algorithm

  • Create an ExecutorService instance using the Executors class.

  • Define a Callable or Runnable implementation that represents the task to be executed.

  • Submit the task to the ExecutorService for execution.

  • Obtain the result if necessary.

Full Ready-to-Execute Code for Approach 2

Example

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyTask implements Callable<Integer> {
   private int[] array;
   private int start;
   private int end;

   public MyTask(int[] array, int start, int end) {
      this.array = array;
      this.start = start;
      this.end = end;
   }

   @Override
   public Integer call() throws Exception {
      int sum = 0;
      for (int i = start; i < end; i++) {
         sum += array[i];
      }
      return sum;
   }
}

public class ExecutorServiceExample {
   public static void main(String[] args) throws Exception {
      int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

      ExecutorService executorService = Executors.newFixedThreadPool(2);

      MyTask task1 = new MyTask(array, 0, 5);
      MyTask task2 = new MyTask(array, 5, 10);
      
      Future<Integer> future1 = executorService.submit(task1);
      Future<Integer> future2 = executorService.submit(task2);

      int result = future1.get() + future2.get();

      executorService.shutdown();

      System.out.println("Result: " + result);
   }
}

Output

Result: 55

Explanation of the Code in Approach 2

In this approach, we create an ExecutorService using the Executors class, which provides a fixed thread pool with two threads. We define a MyTask class that implements the Callable interface, representing the task to be executed. The call() method performs the computation and returns the result. We submit two instances of MyTask to the ExecutorService using the submit() method, which returns a Future object representing the computation's result. Finally, we obtain the results from the Future objects and calculate the final result.

Aspect

Fork/Join Framework

ExecutorService

Syntax

class ForkJoinTask<V> extends Object

interface ExecutorService extends Executor

Design Purpose

Recursive decomposition of tasks

Asynchronous task execution and thread management

Granularity

Best suited for fine-grained tasks

Suitable for both fine-grained and coarse-grained tasks

Task Dependency

Implicitly handles recursive task decomposition

Requires explicit submission of tasks

Parallelism

Utilizes work-stealing algorithm for load balancing

Manages thread pool and task execution

Result Collection

Results are merged in a hierarchical manner

Results are obtained through Future objects

Task Submission

Recursive decomposition within a task

Independent submission of tasks

Control

Limited control over thread management

Greater control over thread management and execution

Use Cases

Divide-and-conquer algorithms, recursive tasks

Concurrent execution of independent tasks

Conclusion

In conclusion, both the Fork/Join Framework and ExecutorService provide powerful mechanisms for concurrent programming in Java. The Fork/Join Framework is designed for recursive decomposition of tasks, particularly suited for problems that can be divided into subtasks. It allows efficient parallel execution by utilizing multiple threads and merging their results. On the other hand, ExecutorService offers a more general-purpose approach to asynchronous task execution, providing thread management and control over the execution environment. It is well-suited for executing independent tasks concurrently and obtaining their results when needed. By understanding the differences and characteristics of these frameworks, developers can choose the most appropriate approach based on their specific requirements, leading to efficient and scalable concurrent Java programs.

Updated on: 28-Jul-2023

164 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements