Interpretation of Fork/Join

Table of contents

concept

key point

use


concept

Fork/Join is a new thread pool implementation added by JDK 1.7. It embodies a divide-and-conquer idea and is suitable for cpu-intensive operations that can split tasks

The so-called task splitting is to split a large task into small tasks with the same algorithm, until it cannot be split and can be solved directly. Some calculations related to recursion, such as merge sort and Fibonacci sequence, can be solved by dividing and conquering

Fork/Join adds multi-threading on the basis of divide-and-conquer, which can decompose and merge each task to different threads to complete, further improving computing efficiency

Fork/Join will create a thread pool with the same size as the number of cpu cores by default

key point

Fork/Join (divide and conquer) is a parallel computing model for solving recursive problems that can be broken down into smaller subtasks. It is a feature introduced in Java 7 and supported by corresponding classes in the java.util.concurrent package.

The Fork/Join model is based on the following two key operations:

  1. Fork (decomposition): Decompose a large task into several small tasks, and put these small tasks into the work queue, waiting to be executed.

  2. Join: Merge the execution results of small tasks to obtain the final result of large tasks.

The core idea of ​​the Fork/Join model is to recursively divide the problem into smaller subproblems until the subproblems are simple enough to be solved directly. Then by combining the results of the sub-problems, the solution to the original problem is finally obtained.

In Java, the implementation of the Fork/Join model mainly depends on the following two classes:

  1. ForkJoinPool : It is the implementation of thread pool, which is used to manage the execution of tasks. It allows creating a group of worker threads, each with its own work queue. Tasks are distributed to idle worker threads for execution, and if a thread's work queue is empty, it can steal tasks from other threads' queues for execution.

  2. RecursiveTask and RecursiveAction : These two abstract classes are used to represent decomposable tasks. RecursiveTask is used for tasks that return results, and RecursiveAction is used for tasks that do not return results. We need to inherit these classes and implement the compute() method to perform task division and merging operations.

Typical usage scenarios include in computationally intensive tasks such as large-scale data processing, image processing, parallel sorting, etc.

The Fork/Join model utilizes the recursive decomposition and merging of tasks, which can make full use of the performance of multi-core processors and provides a simple and efficient parallel computing method.

use

Tasks submitted to the Fork/Join thread pool need to inherit RecursiveTask (with return value) or RecursiveAction (without return value), for example, a task that sums integers between 1 and n is defined below

class AddTask extends RecursiveTask<Integer> {

    int n;

    public AddTask(int n) {
        this.n = n;
    }

    @Override

    public String toString() {
        return "{" + n + '}';
    }

    @Override
    protected Integer compute() {
        // 如果 n 已经为 1,可以求得结果了

        if (n == 1) {
            System.out.println("join()"+n);
          
            return n;
        }

        // 将任务进行拆分(fork)

        AddTask t1 = new AddTask(n - 1);
        t1.fork();
        System.out.println("fork()"+n+" "+ t1);

        // 合并(join)结果

        int result = n + t1.join();
        System.out.println("join()"+n+"+"+t1+"="+result );
        return result;
    }

}

Then submit to ForkJoinPool to execute

public class Test {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        System.out.println(pool.invoke(new AddTask(5)));

    }

}

output

fork()3 {2}
fork()5 {4}
fork()2 {1}
fork()4 {3}
join()1
join()2+{1}=3
join()3+{2}=6
join()4+{3}=10
join()5+{4}=15
15

represented by graph

Improve

class AddTask3 extends RecursiveTask<Integer> {

    int begin;
    int end;

    public AddTask3(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override

    public String toString() {
        return "{" + begin + "," + end + '}';
    }

    @Override

    protected Integer compute() {
        // 5, 5
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        // 4, 5
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }

        // 1 5
        int mid = (end + begin) / 2;

        // 3
        AddTask3 t1 = new AddTask3(begin, mid);

        // 1,3
        t1.fork();
        AddTask3 t2 = new AddTask3(mid + 1, end);
        // 4,5
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);
        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}

Then submit to ForkJoinPool to execute

public static void main(String[] args) {
 ForkJoinPool pool = new ForkJoinPool(4);
 System.out.println(pool.invoke(new AddTask3(1, 10)));
}

result

[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3

[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9

[ForkJoinPool-1-worker-0] - join() 3

[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ?

[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ?

[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6

[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15 15 

 represented by graph

Guess you like

Origin blog.csdn.net/m0_62436868/article/details/131355663