Deep understanding of thread pool Fork Join in concurrent programming

Posted by mazman13 on Thu, 17 Feb 2022 20:16:09 +0100

Deep understanding of thread pool Fork Join in concurrent programming

1, What is Fork Join

Maybe junior programmers don't know what Fork Join is. Let's take a simple example to illustrate:

Suppose we need to send 10W emails in groups, which method may we use? Some people may say that using threads or thread pools, each thread sends 1W emails, which is certainly no problem. Suppose we don't send emails, we may operate 10W complex data processing, and the processing complexity varies according to the data situation. This is 10W data with different execution time. We use 10 threads, There is a small problem. If the tens of thousands of data in front are relatively simple and processed quickly, and the tens of thousands of data in the back are complex, the processing time is three times that of the tens of thousands of data in front. Then the first few threads will complete the execution first and finally wait for the end of the subsequent line execution. Is there a better way for them to evenly allocate the execution time as much as possible? This requires Fork Join.

Fork Join is a framework for parallel task execution provided by Java 7. It is a framework for dividing large tasks into several small tasks. Small tasks can continue to split n more small tasks, and finally summarize the results of small tasks to obtain the results of large tasks.

2, Give an example to illustrate the usage of Fork Join

Let's use the Fork Join thread pool of three threads to find the sum of 1 to 10W to have a simple look:

public class ForkJoinTest extends RecursiveTask<Long> {
    private Long start;		//Start value of calculation
    private Long end;		//Calculated final value
    private Long max = 100L; //Calculate the maximum difference between intervals
    private static AtomicInteger worker1 = new AtomicInteger(0); //Record the execution times of each thread respectively
    private static AtomicInteger worker2 = new AtomicInteger(0);
    private static AtomicInteger worker3 = new AtomicInteger(0);


    public ForkJoinTest(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        Long sum = 0L;
        //If the calculated interval is less than 100, calculate directly
        if (end - start < max) {
            if ("ForkJoinPool-1-worker-1".equals(Thread.currentThread().getName())) {
                worker1.incrementAndGet(); //If thread 1 executes++
                System.out.println("worker1:" + worker1.get());
            }
            if ("ForkJoinPool-1-worker-2".equals(Thread.currentThread().getName())) {
                worker2.incrementAndGet(); //If thread 2 executes++
                System.out.println("worker2:" + worker2.get());
            }
            if ("ForkJoinPool-1-worker-3".equals(Thread.currentThread().getName())) {
                worker3.incrementAndGet(); //If thread 3 executes++
                System.out.println("worker3:" + worker3.get());
            }
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // If the calculated interval > = 100, continue to score in half until the interval of score is less than 100
            Long l = (start + end) / 2; 
            ForkJoinTest left = new ForkJoinTest(start, l);  //Execute the first half
            //Fork() method the Fork() method is similar to thread Start(), but it does not execute the task immediately. Instead, it puts the task into the work queue and splits the molecular task.
            left.fork();
            ForkJoinTest rigt = new ForkJoinTest(l + 1, end); //Execute the latter half
            rigt.fork();
            try {
                sum = left.get() + rigt.get(); //The final result of each half is added up
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinTest forkJoinTest = new ForkJoinTest(1L, 10000L);
        ForkJoinPool forkJoinPool = new ForkJoinPool(3); //The thread pool has 3 threads
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
        System.out.println("The implementation results are:" + submit.get());
    }
}

You can see the calculation results:

If we don't consider threads, let's take a closer look at the logic of the above code. In fact, it is a recursive operation. We recursively divide 10000 into multiple parts with an interval of less than 100 for calculation, and then summarize the final sum. In fact, Fork Join uses multithreaded recursion. First, set an expected value. If it is executed directly within the expected value, if it is not within the expected value, continue recursion and continue recursion tasks.

Fork Join has three ways to submit tasks:

  • For the task submitted through the invoke method, the calling thread will not return until the task execution is completed, that is, this is a synchronous method with returned results;
  • For the task submitted through the execute method, the calling thread will return immediately, that is, it is an asynchronous method and does not return results;
  • For the task submitted through the submit method, the calling thread will return immediately, that is, it is an asynchronous method and has a return result (return the Future implementation class, which can be obtained through get)

Content source: Ant classroom

Topics: Java Back-end Multithreading