Difference between Fork-Join Framework and ExecutorService in Java
In concurrent programming, Java offers many options for programmers to select from. Among the options available, the most widely used and popular choice among programmers is the Fork/ Join framework and ExecutorService. Although both work perfectly in parallelizing the tasks, their use cases differ according to the requirement.
Fork/Join Framework
The Fork/Join framework introduced in Java 7 provides us with tools for task parallelizing. It increases parallel processing by utilizing all the available processor cores. The fork/ join framework gives high and efficient performance using its thread pool, referred to as ForkJoinPool.
The fork/Join framework is practical when dealing with recursive problems where the tasks need to be segregated into multiple sub-tasks and then compute its results. This approach of segregating the tasks is called the divide and conquer strategy.
In the fork/Join framework, the framework first forks. The term “fork” means to divide or split the tasks into sub-tasks. The tasks are split until they are capable enough to run asynchronously.
In the second stage, the framework starts to Join. The term “Join” means that the output from all the sub-tasks is clubbed together recursively into a single result.
The divide and conquer approach work according to the manner shown below.
- Divide the tasks into individual sub-tasks
- Solve the sub-tasks in parallel
- The sub-tasks can run parallelly on available processor cores.
- The sub-tasks can also run concurrently on different threads in a single processor core.
- Wait till the subtask is done
- The join () waits for the sub-task to complete
- Combine or merge the results into a single result.
- The tasks utilize recursive calls to join () and combine the sub-tasks results.
- When a task returns void, the program will wait until every sub-task completes.
Syntax
public class ForkJoin<V> extends Object
Example of Fork/Join Framework
In this example, we will illustrate the functioning of Fork/Join Framework in Java.
Algorithm
- Create the ForkJoinTask subclass depicting the task or operation needed to be executed.
- Use the compute () function in the subclass, splitting the tasks into sub-tasks and calling its execution.
- Merge the results from the sub-tasks to generate the result in a single form.
FileName: ForkJoin.java
// Java program to illustrate the functioning of Fork/Join Framework // import the libraries import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; class FindGreaterTask extends RecursiveTask<Integer> { static final int LIMIT = 8; int[] elements; int startIndex; int endIndex; int element; // Constructor public FindGreaterTask(int[] elements, int startIndex, int endIndex, int element) { this.elements = elements; this.startIndex = startIndex; this.endIndex = endIndex; this.element = element; } @Override protected Integer compute() { // Returns the count computed by countGreater return countGreater(); } // method to find the count of elements greater than // the given element private Integer countGreater() { if (endIndex - startIndex <= LIMIT) { //Set the count variable initially as zero int count = 0; // Traversing using the for loop for (int i = startIndex; i <= endIndex; i++) { // check if the element is greater if (elements[i] > element) { // Increase the count count++; } } // Return the count after the end of the loop return count; } else { // Split the task into sub-tasks int mid = startIndex + (endIndex - startIndex) / 2; FindGreaterTask leftProcess = new FindGreaterTask(elements, startIndex, mid, element); FindGreaterTask rightProcess = new FindGreaterTask(elements, mid, endIndex, element); // Fork the subtasks leftProcess.fork(); rightProcess.fork(); // Merge the results int leftResult = leftProcess.join(); int rightResult = rightProcess.join(); // Return the merged result return leftResult + rightResult ; } } } // The Main class public class ForkJoin { // main method public static void main(String[] args) { // Array elements int[] elements = { 11, 21, 43, 22, 0, 777, 1, 64, 72, 4, 8, 211, 122, 6}; //We need to find elements greater than 21 int element = 21; // stores the start and endIndex indexes int startIndex = 0; int endIndex = elements.length - 1; //Create the instance or object of the ForkJoinPool class ForkJoinPool obj = ForkJoinPool.commonPool(); // Create an object of the FindGreaterTask class FindGreaterTask task = new FindGreaterTask(elements, startIndex, endIndex, element); int result = obj.invoke(task); // Display the count of elements greater than the given element System.out.println("The count of elements greater than "+element +" is " +result); } }
Output:
The count of elements greater than 21 is 7
Explanation:
In the above example, we created the FindGreaterTask class that extends the RecursiveTask<Integer> to run computations and get the result. In the FindGreaterTask class, we override the compute() function to split the tasks into smaller individual sub-tasks or processes whenever they surpass the set LIMIT. After splitting, the sub-tasks are forked, and their results are merged to generate the final result.
ExecutorService
The ExecutorService in Java is an interface that allows us to run tasks or processes asynchronously. In principle, the ExecutorService offers a thread pool and API for allocating the tasks.
Since the ExecutorService extends the Executor, we can use its Runnable method, but there are way more methods in the ExecutorService.
The shutdown() method will inform the ExecutorService to stop taking new tasks and eventually shut itself after all the threads finish its execution.
The submit() method will assign the ExecutorService the task and return its result of type Future.
The ExecutorService approach works according to the manner shown below.
- Submit the task using submit() and return the result, which will be of type Future.
- Handle the executor service and tasks lifecycle.
- The object of the ExecutorService can be present in one of the following 3 states:
- The Running state – After we create the instance or object
- The Shutting down state – After we shut it down using the shutdown() method. Also, it can be due to any fault or system failure.
- The terminated state - After all the tasks complete themselves.
Syntax
public interface ExecutorService extends Executor
Example of ExecutorService
In this example, we will illustrate the functioning of ExecutorService in Java.
Algorithm
- Create an object of the ExecutorService by utilizing the Executor class.
- Define and implement callable implementation that will depict the tasks that need to be processed. We can also use Runnable instead of Callable.
- Submit the tasks.
- Get the results
- Shut down the service.
FileName: MyExecutorService.java
// Java program to illustrate the functioning of ExecutorService // Import the libraries or packages import java.util.concurrent.*; class FindGreaterTask implements Callable<Integer> { int[] elements; int startIndex; int endIndex; int element; // Constructor public FindGreaterTask(int[] elements, int startIndex, int endIndex, int element) { this.elements = elements; this.startIndex = startIndex; this.endIndex = endIndex; this.element = element; } @Override public Integer call() { // Returns the count computed by countGreater return countGreater(); } // method to find the count of elements greater than // the given element private Integer countGreater() { //Set the count variable initially as zero int count = 0; // Traversing using the for loop for (int i = startIndex; i < endIndex; i++) { // check if the element is greater if (elements[i] > element) { // Increase the count count++; } } // Return the count after the end of the loop return count; } } // The Main class public class MyExecutorService { // main method public static void main(String[] arg) throws ExecutionException, InterruptedException { // Array elements int[] elements = { 11, 21, 43, 22, 0, 777, 1, 64, 72, 4, 8, 211}; //We need to find elements greater than 21 int element = 21; // Create an instance of the Executor Service class of thread pool size 3 ExecutorService service = Executors.newFixedThreadPool(3); // Create an object of the FindGreaterTask class FindGreaterTask t1 = new FindGreaterTask(elements, 0, 4, element); FindGreaterTask t2 = new FindGreaterTask(elements, 4, 8, element); FindGreaterTask t3 = new FindGreaterTask(elements, 8, elements.length, element); Future<Integer> f1 = service.submit(t1); Future<Integer> f2 = service.submit(t2); Future<Integer> f3 = service.submit(t3); int finalResult = f1.get() + f2.get() + f3.get(); //Shut down the executor service after the task is completed service.shutdown(); // Display the count of elements greater than the given element System.out.println("The count of elements greater than "+element +" is " +finalResult); } }
Output:
The count of elements greater than 21 is 6
Explanation:
In the above example, we have created an ExecutorService using the Executors. The ExecutorService creates a thread pool of fixed size 3 in which we can run our tasks. The FindGreaterTask that implements Callable contains the call() method that depicts the task that needs to be performed. The 3 objects of the FindGreaterTask are submitted to the ExecutorService using the submit() that returns an object of type Future. After the task, we shut down the ExecutorService using shutdown(), indicating that the task is completed.
Let us go through some differences between the Fork/Join framework and ExecutorService in Java.
Fork/ Join Framework | ExecutorService |
The primary purpose of the Fork/ Join framework is to split or divide the tasks and solve them recursively. | The primary purpose of the ExecutorService is to provide Asynchronous processing of the tasks and manage the threads. |
Its syntax is: public class MyForkJoin<V> extends Object | Its syntax is: public interface ExecutorService extends Executor |
The Fork/Join framework is an implementation of the ExecutorService and was introduced in Java 7. | ExecutorService was introduced in Java 5 and is present in Java.util.concurrent. |
The Fork/Join framework implicitly manages the splitting of recursive tasks. | In ExecutorService, we must explicitly submit tasks using the submit() method. |
The Fork/Join framework utilizes the RecursiveTask class for returning the result. | The ExecutorService utilizes the callable interface. |
The Fork/ Join framework utilizes the Work Stealing Algorithm. Due to this, in the fork/ Join framework, when a task is waiting for the sub-tasks to complete, the worker thread processing the task tries to find another task that has not been processed yet and thus steals it and executes it. | In contrast to the Fork/Join framework, when the task is waiting for the sub-tasks to complete itself, the worker thread processing the task does not try to find and steal another task. |
The Fork/Join framework utilizes resources efficiently as it uses the work-stealing algorithm to steal other tasks. | It does weaker utilization of resources than the Fork/Join framework. |
The Fork/Join framework provides restricted access for managing and handling the thread. | The ExecutorService provides greater access for managing and handling the thread. |
The Fork/Join framework is more appropriate for fine-grained tasks. | The ExecutorService is appropriate for both the coarse and fine-grained tasks. |
The results are combined in a hierarchical pattern as it involves recursion. | The results are acquired using objects of type Future. |