Course Content
Multithreading in Java
Multithreading in Java
ForkJoinPool
The ForkJoinPool
class in Java for working with the Fork/Join
framework is just the realization of this. It provides mechanisms for managing tasks that can be divided into smaller subtasks and executed in parallel.
Main
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinPool
is based on the concept of two basic actions: fork and join. Fork
is an action where we break a large task into several smaller subtasks that can be executed in parallel. Join
is the process by which the results of these subtasks are combined together to form the result of the original task.
Case Study
Imagine that you need to analyze a huge data set that can be divided into several smaller sets. If you process each data set separately and then merge the results, you can speed up the processing significantly, especially on multiprocessor systems.
How to use ForkJoinPool
There are 2 classes to implement tasks in them, they are RecursiveTask
and RecursiveAction
. Both of them require an abstract compute()
method to be implemented. In RecursiveTask
the compute()
method returns a value, while in RecursiveAction
the compute()
method returns void.
Main
class ExampleTask extends RecursiveTask<String> { @Override protected String compute() { // code return null; } }
When we inherit from a RecursiveTask
we must be sure to specify what type of data our compute()
method will return using this syntax RecursiveTask<String>
.
Main
class ExampleAction extends RecursiveAction { @Override protected void compute() { // code } }
Here it's the opposite, we don't need to specify the type explicitly in RecursiveAction
, because our method won't return anything and will just perform the task.
Starting a Task
By the way, we can start a task for execution without even using ForkJoinPool
, just using fork()
and join()
methods.
It is important to note that the fork()
method sends the task to some thread. The join()
method is used to get the result.
Main
public static void main(String[] args) { ExampleTask simpleClass = new ExampleTask(); simpleClass.fork(); System.out.println(simpleClass.join()); }
ForkJoinPool Main Methods
invoke()
: Starts a task in the pool and waits for it to complete. Returns the result of the task's completion;submit()
: Submits a task to the pool, but does not block the current thread while waiting for the task to complete. Can be used to submit tasks and retrieve theirFuture
objects;execute()
: Runs a task in the pool, but does not return a result and not block the current thread.
How can we start a task using ForkJoinPool
,very simple!
Main
public static void main(String[] args) { ExampleAction simpleClass = new ExampleAction(); ForkJoinPool forkJoinPool = new ForkJoinPool(); System.out.println(forkJoinPool.invoke(simpleClass)); }
But what is the difference between running fork()
, join()
and via the ForkJoinPool
class?
It's very simple! When using the first method with the help of fork()
, join()
methods, we start the task in the same thread in which these methods were called, blocking this thread, while with the help of ForkJoinPool
class we are allocated a thread from the pool and it works in this thread without blocking the main thread.
Let's take a closer look, let's assume we have such an implementation:
Main
class ExampleRecursiveTask extends RecursiveTask<String> { @Override protected String compute() { System.out.println("Thread: " + Thread.currentThread().getName()); return "Wow, it works!!!"; } }
And we want to run this code using fork()
and join()
. Let's see what will be printed to the console and which thread will perform this task.
Main
public class Main { public static void main(String[] args) { ExampleRecursiveTask simpleClass = new ExampleRecursiveTask(); simpleClass.fork(); System.out.println(simpleClass.join()); } }
And we get this output to the console:
Let's now check what happens if we run with ForkJoinPool
:
Main
public class Main { public static void main(String[] args) { ExampleRecursiveTask simpleClass = new ExampleRecursiveTask(); ForkJoinPool forkJoinPool = new ForkJoinPool(); System.out.println(forkJoinPool.invoke(simpleClass)); } }
And we get this conclusion:
And as you can see the difference is that using the 1st method the task is executed in the same thread that called this task (Main thread). But if we use the 2nd method, the thread is taken from the ForkJoinPool
thread pool and the main thread that called this logic is not blocked and goes on!
How to Implement Fork/Join in Code
The easiest way to explain this would be in a video, rather than giving you 50-80 lines of code and explaining it point by point.
Summary
ForkJoinPool
is effective for tasks that can be easily divided into smaller subtasks. However, if the tasks are too small, usingForkJoinPool
may not bring a significant performance gain.
Thanks for your feedback!