Detailed Explanation of Fork-Join Framework of Java Concurrency

This article is reproduced from: https://www.cnblogs.com/senlinyang/p/7885964.html

      The Fork-Join framework is a framework provided by Java 7 for executing tasks in parallel. It is a framework that divides a large task into several small tasks, and finally summarizes the results of each small task to obtain the results of the large task. The Fork-Join framework does two things:

  1. Task division: First, the Fork-Join framework needs to divide large tasks into sufficiently small sub-tasks. If the sub-tasks are relatively large, the sub-tasks should be further divided.

  2. Execute the task and merge the results: The divided subtasks are placed in the double-ended queue respectively, and then several startup threads obtain the task execution from the double-ended queue respectively. The results of the subtasks are placed in another queue, start a thread to fetch data from the queue, and then merge the data.

      In Java's Fork-Join framework, the above operations are done using two classes:

  1.ForkJoinTask: To use the Fork-Join framework, we first need to create a ForkJoin task. This class provides mechanisms to perform fork and join on tasks. Usually, we do not need to directly integrate the ForkJoinTask class, but only need to inherit its subclasses. The Fork-Join framework provides two subclasses:
    a.RecursiveAction: for tasks that do not return results
    b.RecursiveTask: for tasks that return results Task
  2.ForkJoinPool: ForkJoinTask needs to be executed through ForkJoinPool

  The subtasks divided by the task will be added to the double-ended queue maintained by the current worker thread and enter the head of the queue. When there is no task in the queue of a worker thread, it will randomly acquire a task from the tail of the queue of other worker threads (work-stealing algorithm).

Implementation principle of Fork-Join framework

      ForkJoinPool consists of ForkJoinTask array and ForkJoinWorkerThread array. ForkJoinTask array is responsible for submitting the stored program to ForkJoinPool, and ForkJoinWorkerThread is responsible for executing these tasks.

      The implementation principle of the Fork method of ForkJoinTask:

  When we call the fork method of ForkJoinTask, the program will put the task in the workQueue of the pushTask of ForkJoinWorkerThread, execute the task asynchronously, and then return the result immediately. The code is as follows:

public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }

  The pushTask method stores the current task in the ForkJoinTask array queue. Then call the signalWork() method of ForkJoinPool to wake up or create a worker thread to perform the task. code show as below:

final void push(ForkJoinTask<?> task) {
            ForkJoinTask<?>[] a; ForkJoinPool p;
            int b = base, s = top, n;
            if ((a = array) != null) {    // ignore if queue removed
                int m = a.length - 1;     // fenced write for task visibility
                U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
                U.putOrderedInt(this, QTOP, s + 1);
                if ((n = s - b) <= 1) {
                    if ((p = pool) != null)
                        p.signalWork(p.workQueues, this);
                }
                else if (n >= m)
                    growArray();
            }
        }

      The implementation principle of the join method of ForkJoinTask:

      The main function of the Join method is to block the current thread and wait for the result. Let's take a look at the implementation of the join method of ForkJoinTask, the code is as follows:

public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
      It first calls the doJoin method, and uses the doJoin() method to get the status of the current task to determine what result to return. There are 4 types of task status: completed (NORMAL), canceled (CANCELLED), signal (SIGNAL) and exception (EXCEPTIONAL) .
  If the task status is completed, the task result will be returned directly;
  if the task status is canceled, a CancellationException will be thrown directly;

  If the task state is to throw an exception, the corresponding exception is thrown directly.

      Let's analyze the implementation of the doJoin method:

private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }
final int doExec() {
        int s; boolean completed;
        if ((s = status) >= 0) {
            try {
                completed = exec();
            } catch (Throwable rex) {
                return setExceptionalCompletion(rex);
            }
            if (completed)
                s = setCompletion(NORMAL);
        }
        return s;
    }

      In the doJoin() method, first check the status of the task to see if the task has been executed. If the execution is complete, the task status is returned directly; if not, the task is taken out of the task array and executed. If the task is successfully executed, set the task status to NORMAL, if an exception occurs, record the exception and set the task status to EXCEPTIONAL.

Exception Handling of Fork-Join Framework

      ForkJoinTask may throw exceptions during execution, but we can't catch exceptions directly in the main thread, so ForkJoinTask provides isCompletedAbnormally() method to check whether the task has thrown an exception or has been canceled, and can pass ForkJoinTask's The getException method gets the exception. Use the following code:

if(task.isCompletedAbnormally())
{
    System.out.println(task.getException());
}

      The getException method returns a Throwable object, or CancellationException if the task is canceled. Returns null if the task did not complete or an exception was not thrown:

public final Throwable getException() {
        int s = status & DONE_MASK;
        return ((s >= NORMAL)    ? null :
                (s == CANCELLED) ? new CancellationException() :
                getThrowableException());
    }
Example of use:
      We introduce the use of the Fork/Join framework through a simple example. Demand is the result of finding 1+2+3+4.

      The first thing to consider when using the Fork/Join framework is how to divide the task. If you want each subtask to perform the addition of at most two numbers, then we set the threshold of the division to 2. Since it is the addition of 4 numbers, Fork/Join The framework will fork this task into two subtasks, subtask one is responsible for calculating 1+2, subtask two is responsible for calculating 3+4, and then joins the results of the two subtasks. Because it is a task with a result, it must inherit RecursiveTask. The implementation code is as follows:

public class CountTask extends RecursiveTask<Integer>{
    private static final int THREAD_HOLD = 2;
    private int start;
    private int end;

    public CountTask(int start,int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // Calculate if the task is small enough
        boolean canCompute = (end - start) <= THREAD_HOLD;
        if(canCompute){
            for(int i=start;i<=end;i++){
                sum += i;
            }
        }else{
            int middle = (start + end) / 2;
            CountTask left = new CountTask(start,middle);
            CountTask right = new CountTask(middle+1,end);
            // execute subtask
            left.fork();
            right.fork();
            //Get the subtask result
            int lResult = left.join();
            int rResult = right.join();
            sum = lResult + rResult;
        }
        return sum;
    }

    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool();
        CountTask task = new CountTask(1,4);
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace ();
        } catch (ExecutionException e) {
            e.printStackTrace ();
        }
    }
}

      Through this example, we further understand ForkJoinTask. The main difference between ForkJoinTask and general tasks is that it needs to implement the compute method. In this method, we first need to determine whether the task is small enough, and if it is small enough, execute the task directly. If it is not small enough, it must be divided into two subtasks. When each subtask calls the fork method, it will enter the compute method again to see if the current subtask needs to be further divided into subtasks. If no further division is required, the current subtask will be executed. task and return the result. Using the join method will wait for the subtask to finish executing and get its result.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325898908&siteId=291194637