What is ForkJoinPool?
When talking about thread pools, many people will think of the preset thread pools provided by Executors, such as SingleThreadExecutor and FixedThreadPool, but few will notice that there’s a special thread pool available: WorkStealingPool we click on this method and see that, unlike other methods, this thread pool is created by ForkJoinPool rather than by ThreadPoolExecutor:
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
Copy the code
The relationship between the two thread pools is not inherited, but horizontal:
Divide and conquer method
For example, if we want to count the sum from 1 to 100, we can use ForkJoinPool to divide the sum from 1 to 100 into 20 tasks by dividing them into five segments. Each of the 20 tasks calculates only the results of its own segment. The total results of the 20 tasks are the sum from 1 to 100
How do I use ForkJoinPool?
ForkJoinPool is essentially two things:
- If the task is small: calculate the result directly
- If the task is large:
- Break it down into N subtasks
- Call fork() of the subtask to compute
- Call the subtask’s Join () to merge the results
Let’s do a 1-100 summation example:
- Start by defining the tasks we need to perform:
class Task extends RecursiveTask<Integer> {
private int start;
private int end;
private int mid;
public Task(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if(end-start < 6) {// If the task is very small, start the calculation directlyfor (int i = start; i <= end; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " count sum: " + sum);
} else{// Otherwise, split the task mid = (end-start) / 2 + start; Task left = new Task(start, mid); Task right = new Task(mid + 1, end); // execute the subtask left. Fork (); right.fork(); Sum += left. Join (); sum += right.join(); }returnsum; }}Copy the code
ForkJoinTask is a subclass of ForkJoinTask. ForkJoinTask is a subclass of Future
We start by defining in the Task class some data that the Task needs, such as the start and end positions. The key is the compute method, which implements the steps we just described. If the task is small (as judged by the task data), perform the calculation, otherwise split the task, fork(), and join()
- Submit the task to the thread pool
Now that we have defined the task class, we need to submit the task to the thread pool:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
Task countTask = new Task(1, 100);
ForkJoinTask<Integer> result = forkJoinPool.submit(countTask);
System.out.println("result: " + result.get());
forkJoinPool.shutdown();
}
Copy the code
Note that a ForkJoinPool initialization may pass in a parallel parameter, otherwise the number of processors is used as the parallel parameter by default
After creating the task object and thread pool, submit the task using the Submit method, which returns a ForkJoinTask
object and calls its GET method to retrieve the result
Also note that the thread pool also needs to be shutdown by calling the shutdown method
The principle of ForkJoinPool
ForkJoinPool has three important roles:
- ForkJoinWorkerThread: A worker Thread that encapsulates the Thread internally
- WorkQueue: indicates a task queue
- ForkJoinTask: Inherited from Future, ForkJoinTask is a submission or task
In the thread pool, the task queue is held in an array that holds all submitted tasks:
- Save submission in the odd position
- Save tasks in even positions
Submission refers to a locally submitted task, such as submit and execute. Tasks are subtasks added through the fork method. The two tasks are only different in meaning, so they are stored together in the task queue and distinguished by location
The core of ForkJoinPool
To understand ForkJoinPool, it is necessary to understand that there are two core elements. One is divide-and-conquer, and the other is the job-stealing algorithm. Divide-and-conquer is believed to increase concurrency by breaking up large tasks into smaller ones. The focus is on the job-stealing algorithm, which works by:
All threads attempt to find and execute submitted tasks or subtasks created from other tasks
Rely on this feature to avoid situations where a thread “sits idle” after completing its own task. Meanwhile, the steal order is FIFO