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:

  1. If the task is small: calculate the result directly
  2. 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:

  1. 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()

  1. 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:

  1. Save submission in the odd position
  2. 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