A thorough understanding of the product of JUC divide and conquer thought of concurrency -- an analysis of the principle of ForkJoin branch merger framework Part II

Posted by *Lynette on Sun, 07 Nov 2021 01:34:32 +0100

introduction

In< (12) A thorough understanding of the product of JUC divide and conquer thought of concurrency -- an analysis of the principle of ForkJoin branch merger framework part I >In, we had a preliminary understanding of the use of ForkJoin branch merging framework, and also analyzed the membership of the framework and the principle and implementation of task submission and creation. In this article, we will analyze the source code implementation of task execution, task scanning, thread suspension, result merging and task theft of the framework.

1, Worker thread execution task / work stealing implementation process

At the end of the previous article, the principle and implementation of worker thread registration and destruction are analyzed from signalwork() - > tryaddworker() - > createWorker() - > newthread() - > forkjoinworkerthread() - > registerworker() - > deregisterworker(). Next, continue to analyze the task execution process of the worker thread. First return to the previous createworker () method:

// ForkJoinPool class → createWorker() method
private boolean createWorker() {
    ForkJoinWorkerThreadFactory fac = factory;
    Throwable ex = null;
    ForkJoinWorkerThread wt = null;
    try {
        if (fac != null && (wt = fac.newThread(this)) != null) {
            // If the creation is successful, the start() method is called to execute
            wt.start();
            return true;
        }
    } catch (Throwable rex) {
        ex = rex;
    }
    // If an exception occurs during the creation process, the thread is unregistered
    deregisterWorker(wt, ex);
    return false;
}

It is obvious that after the thread is created successfully, the start() method will be called to execute the task, and finally the run() method will be found to execute it:

// ForkJoinWorkerThread class → run() method
public void run() {
    // If the task array is not empty
    if (workQueue.array == null) {
        Throwable exception = null;
        try {
            // Hook function, used to expand, here is an empty implementation
            onStart();
            // Use the runWorker method of the thread pool to execute the queue task
            pool.runWorker(workQueue);
        } catch (Throwable ex) {
            // If an exception occurs during execution, record it first
            exception = ex;
        } finally {
            try {
                // Hook function, reporting exception
                onTermination(exception);
            } catch (Throwable ex) {
                if (exception == null)
                    exception = ex;
            } finally {
                // If an exception occurs during execution, log off the thread
                pool.deregisterWorker(this, exception);
            }
        }
    }
}

// ForkJoinPool class → runWorker() method
final void runWorker(WorkQueue w) {
    // Initialize the task array. The task array was not initialized at first
    // This method initializes or doubles the capacity of the array
    w.growArray();  
    // Gets the random seed used to calculate the index recorded when registering the queue
    int seed = w.hint;
    // If the seed is 0, change to 1 to avoid using 0
    int r = (seed == 0) ? 1 : seed; 
    // Dead cycle
    for (ForkJoinTask<?> t;;) {
        // Scan task: randomly select the work queue in the queue array of the pool to obtain the task execution
        if ((t = scan(w, r)) != null)
            // If the task is obtained, execute
            w.runTask(t);
        // If no tasks are scanned, try spin or suspend blocking
        else if (!awaitWork(w, r))
            break;
        // After each execution, modify the random value and change a queue to obtain the task
        r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
    }
}

// ForkJoinPool class → scan() method
private ForkJoinTask<?> scan(WorkQueue w, int r) {
    WorkQueue[] ws; int m;
    // If the queue array is not empty and the task queue has been initialized and is not empty
    if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
        // Gets the current queue scanState, starting with the index of the queue in the array
        int ss = w.scanState; 
        // R & M: randomly obtain the subscript in an array, oldSum/checkSum: the identification of the comparison validation sum
        // Open cycle
        for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
            WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
            int b, n; long c;
            // If the random subscript position queue is not empty
            if ((q = ws[k]) != null) {
                // Determine whether there are tasks in the queue
                if ((n = (b = q.base) - q.top) < 0 &&
                    (a = q.array) != null) {
                    // FIFO mode, calculate the stack bottom / team head position through the memory offset
                    long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                    // Get the task at the bottom of the stack
                    if ((t = ((ForkJoinTask<?>)
                              U.getObjectVolatile(a, i))) != null &&
                        q.base == b) {
                        // If the worker thread is active
                        if (ss >= 0) {
                            // Try to preempt threads using CAS mechanism (there may be multiple threads)
                            if (U.compareAndSwapObject(a, i, t, null)) {
                                // After the preemption task is successful, move the bottom of the stack to a position
                                // It is convenient for other threads to continue to obtain tasks
                                q.base = b + 1;
                                // If there are other tasks left in the queue
                                if (n < -1)       // signal others
                                    // Create or wake up a thread to continue processing
                                    signalWork(ws, q);
                                return t;
                            }
                        }
                        // If the current thread is not activated, it is blocked
                        else if (oldSum == 0 &&   // try to activate
                                 w.scanState < 0)
                             // Wake up thread
                            tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                    }
                    // Update the scanState value once (because the thread may have been awakened earlier)
                    if (ss < 0)                   // refresh
                        ss = w.scanState;
                    // Gets a new random value for the next random index position
                    r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
                    // A new subscript index is calculated according to the new random seed
                    origin = k = r & m;           // move and rescan
                    // Validation and identification reset
                    oldSum = checkSum = 0;
                    // End this cycle and continue the next cycle
                    continue;
                }
                // If the task is not obtained, checkSum+1 indicates that a location has been traversed
                // For validation
                checkSum += b;
            }
            // K = (K + 1) &m represents to go to the next position of the queue array and continue to find the queue of the next pit,
            // If = = origin, it means that all queues have been traversed
            if ((k = (k + 1) & m) == origin) {    // continue until stable
                // If the worker thread is still active and the entire queue is scanned,
                // The validation and have not changed, which means that no new tasks have been submitted to the thread pool
                if ((ss >= 0 || (ss == (ss = w.scanState))) &&
                    oldSum == (oldSum = checkSum)) {
                    // If the active state becomes < 0, it means that it is already inactive
                    // Then exit the scan, return null, and return to runWorker() to block the thread
                    if (ss < 0 || w.qlock < 0)    // already inactive
                        break;
                    // Inactivation operation (the thread after inactivation is called inactivation state):
                    //      First change the current scanState to a negative number
                    int ns = ss | INACTIVE;       // try to inactivate
                    // Subtract the number of active threads from the ctl,
                    // And the inactivated ss is saved to the lower 32 bits of ctl
                    long nc = ((SP_MASK & ns) |
                               (UC_MASK & ((c = ctl) - AC_UNIT)));
                    // Use the stackPred member of the worker thread to save the of the last inactivated thread
                    // scanState to form a blocking stack, and the lower 32 bits of ctl save the top of the stack
                    w.stackPred = (int)c;         // hold prev stack top
                    // Update the scanState of the current worker thread
                    U.putInt(w, QSCANSTATE, ns);
                    // Update ctl value using cas mechanism
                    if (U.compareAndSwapLong(this, CTL, c, nc))
                        ss = ns;
                    else
                        // If the update fails, exit rollback and continue to scan the task because of the cas process
                        // In, there is only one reason for the failure: the ctl value has changed
                        // Change, this may be after a new task is submitted, wake up or
                        // Added a thread
                        w.scanState = ss;         // back out
                }
                // Check identification reset
                checkSum = 0;
            }
        }
    }
    // If the task is not scanned, null is returned directly and the runWorker() outside is blocked
    return null;
}

ok, the above is the source code implementation of the whole thread work, focusing on the implementation process of task scanning. At the same time, it is also a difficult place to understand. Let's comb the whole thread work and scanning task process as a whole:

  • ① After the thread start() starts, it will find the run() method, and then start competing for shared tasks in the thread pool
  • ② Initialize the work queue of the thread, and obtain the random seed of the index calculated when registering the queue
  • ③ Turn on cyclic scanning and calculate a subscript index in the queue array in the pool through random seed
  • ④ Judge whether the random index position is empty:
    • Not empty: judge whether there are tasks in the queue:
      • Exist: judge whether the current thread state is active:
        • Yes: take out the tasks at the bottom of the stack / at the head of the queue in FIFO mode through cas mechanism. If there are still tasks left, create or wake up a new thread to continue processing, and then return to the obtained task
        • No: first wake up the inactive thread recorded in ctl, randomly calculate a new position, jump out of this cycle and continue the next cycle
      • Does not exist: update scanState and randomly calculate a new location, jump out of this cycle and continue the next cycle
      • If there is a task but no task is obtained, it means that no other thread has robbed the task. checkSum+1 randomly calculates a new location, jumps out of this cycle and continues the next cycle
    • Empty: indicates that there is no queue at this location, find the next location, and so on
  • ④ If the queue is empty, the next location will be found, and then repeat step ④
  • ⑤ If no task is obtained after traversing all queues, and no new task is submitted to the thread pool during scanning, judge the active state of the worker thread first:
    • Inactive state: exit the loop directly and return to runWorker() method spin or suspend blocking
    • Active state: perform the inactivation operation, use the cas mechanism to subtract the number of active threads in ctl, record the scanState value of the current thread to the lower 32 bits of ctl as the top of the stack, and use stackPred to save the scanState value of the last inactivated thread, so as to form a blocking stack
    • If the inactivation operation fails, it means that the ctl has changed and a new task has been submitted to the thread pool, then the inactivation operation is cancelled
    • If the thread is active, it will scan all queues again after scanning for a circle and no task is obtained. In the second scan, the thread has the opportunity to be "resurrected (awakened)"
  • ⑥ If the task is still not obtained after the second round of scanning, the current thread will exit the loop and return null
  • ⑦ After scanning, return to the runWorker() method, where you will judge whether the scanning result is empty:
    • Non empty: call runTask() to execute the task obtained by scanning
    • Null: call awaitWork() to spin or suspend the blocking thread until a new task is submitted and wakes up
  • ⑧ If the task is obtained successfully and an exception occurs during execution, the exception information is reported and the thread is logged off

The whole process of thread work and scanning will be relatively long. In particular, some small partners have some difficulties in understanding the multiple scans of the scan() method. If the thread does not obtain the task during the first cycle of scanning, it will be inactivated first and then scanned for one cycle. If the task is scanned in the second cycle, it will "revive" the inactivated thread and then scan for another cycle. If the task is not scanned in the second round of scanning, exit the cycle directly. Here is a flow chart to deepen understanding:

In fact, the implementation of scanning also includes the implementation of task stealing, because in the process of scanning, even queues and odd queues will not be distinguished, and all queues will be scanned. As long as there are tasks, they will be obtained and executed, and the way to obtain tasks is through FIFO, It means that the task acquisition and work stealing in the shared queue are realized by obtaining the elements at the head / bottom of the queue. When a thread executes a task in its own work queue, it uses the LIFO mode to obtain task execution from the end of the queue / top of the stack. The advantage of this is that it can avoid work theft and CAS competition during local execution.

ok, let's take a look at the implementation of task execution and thread suspension:

// FrokJoinPool class → runTask() method
final void runTask(ForkJoinTask<?> task) {
    // If the task is not empty
    if (task != null) {
        // scanState & = ~ scanning will change scanState to an even number, indicating that the task is being executed
        scanState &= ~SCANNING; // mark as busy
        // Perform tasks
        (currentSteal = task).doExec();
        // After the task is executed, the member of the stolen task will be left blank
        U.putOrderedObject(this, QCURRENTSTEAL, null);
        // Execute local tasks: tasks in the worker thread's own queue
        execLocalTasks();
        ForkJoinWorkerThread thread = owner;
        // Steal task count
        if (++nsteals < 0)
            // Superimposed on the steelcounter member of ForkJoinPool
            transferStealCount(pool);
        // After execution, change the status from execution to scanning again
        scanState |= SCANNING;
        // Execute hook function
        if (thread != null)
            thread.afterTopLevelExec();
    }
}

// FrokJoinPool class → execLocalTasks() method
final void execLocalTasks() {
    int b = base, m, s;
    ForkJoinTask<?>[] a = array;
    // If there are tasks in your work queue
    if (b - (s = top - 1) <= 0 && a != null &&
        (m = a.length - 1) >= 0) {
        // If the self queue is specified as FIFO mode execution
        if ((config & FIFO_QUEUE) == 0) {
            for (ForkJoinTask<?> t;;) {
                // Get task execution from stack top / queue header
                if ((t = (ForkJoinTask<?>)U.getAndSetObject
                     (a, ((m & s) << ASHIFT) + ABASE, null)) == null)
                    break;
                U.putOrderedInt(this, QTOP, s);
                //Perform tasks
                t.doExec();
                if (base - (s = top - 1) > 0)
                    break;
            }
        }
        else
            // If not, get the execution task directly from the bottom of the stack in LIFO mode
            pollAndExecAll();
    }
}
// FrokJoinPool class → pollandexecalll() method
final void pollAndExecAll() {
    // Get task at bottom of stack / end of queue
    for (ForkJoinTask<?> t; (t = poll()) != null;)
        t.doExec();
}
// WorkerQueue class → poll() method
final ForkJoinTask<?> poll() {
    ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
    // Task queue is not empty
    while ((b = base) - top < 0 && (a = array) != null) {
        // Value from bottom of stack / end of queue
        int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
        t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
        // Check whether it is preempted by other threads
        if (base == b) {
            if (t != null) {
                // Empty
                if (U.compareAndSwapObject(a, j, t, null)) {
                    base = b + 1;
                    return t;
                }
            }
            // If there are no tasks in the queue, exit
            else if (b + 1 == top) // now empty
                break;
        }
    }
    return null;
}

// FrokJoinPool class → awaitWork() method
private boolean awaitWork(WorkQueue w, int r) {
    // If the queue has been logged off, return directly
    if (w == null || w.qlock < 0)
        return false;
    // Open loop (w.stackPred: scanState value of the last blocked thread)
    for (int pred = w.stackPred, spins = SPINS, ss;;) {
        // If the current thread is "resurrected / awakened", it will exit directly
        if ((ss = w.scanState) >= 0)
            break;
        
        // Spin operation: random spin for a period of time before suspending the thread
        else if (spins > 0) {
            // Random spin is realized by random seed and spin number
            r ^= r << 6; r ^= r >>> 21; r ^= r << 7;
            // Check whether the previous inactivated suspended worker thread has been revived
            if (r >= 0 && --spins == 0) {  // randomize spins
                WorkQueue v; WorkQueue[] ws; int s, j; AtomicLong sc;
                if (pred != 0 && (ws = workQueues) != null &&
                    (j = pred & SMASK) < ws.length &&
                    (v = ws[j]) != null &&        // see if pred parking
                    (v.parker == null || v.scanState >= 0))
                    spins = SPINS;                // continue spinning
            }
        }
        
        // Check the queue status again and check whether it is logged off
        else if (w.qlock < 0)    // recheck after spins
            return false;
        // If the thread is not interrupted
        else if (!Thread.interrupted()) {
            long c, prevctl, parkTime, deadline;
            // Gets the number of active threads
            int ac = (int)((c = ctl) >> AC_SHIFT) + (config & SMASK);
            // If the number of active threads is < = 0, the thread pool may be closed. Here we will help close it
            if ((ac <= 0 && tryTerminate(false, false)) ||
                (runState & STOP) != 0)           // pool terminating
                return false;
            // If the number of active threads < = 0 and the current thread is the last suspended thread
            if (ac <= 0 && ss == (int)c) {        // is last waiter
                // Calculate a ctl value
                prevctl = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & pred);
                // Get bus number
                int t = (short)(c >>> TC_SHIFT);  // shrink excess spares
                // If the number of buses is greater than 2, there are more than two suspended threads
                // The current thread will be discarded
                if (t > 2 && U.compareAndSwapLong(this, CTL, c, prevctl))
                    // When false is returned, the external runWorker() will directly break out,
                    // This causes run() to end and the thread to die
                    return false;
                // If the number of suspended threads is < = 2 or cas fails (some threads are awakened / revived)
                // Then the suspension time is calculated and the current thread is suspended for a period of time
                // Calculate suspend time
                parkTime = IDLE_TIMEOUT * ((t >= 0) ? 1 : 1 - t);
                // Calculation end time
                deadline = System.nanoTime() + parkTime - TIMEOUT_SLOP;
            }
            // If there are still active threads or the current thread is not the last suspended thread
            else
                // Suspend the current thread all the time (after such permanently suspended threads are awakened, if
                // scanState or inactive state should be, which may be that the thread pool is shutting down)
                prevctl = parkTime = deadline = 0L;
            // Get current thread
            Thread wt = Thread.currentThread();
            
            // Simulate LockSupport.park() pending operation
            U.putObject(wt, PARKBLOCKER, this);   // emulate LockSupport
            w.parker = wt;
            // The status is checked again before suspending
            if (w.scanState < 0 && ctl == c)      // recheck before park
                // Pending operation
                U.park(false, parkTime);
            U.putOrderedObject(w, QPARKER, null);
            U.putObject(wt, PARKBLOCKER, null);
            // If it is resurrected, it will exit the loop directly and return true
            if (w.scanState >= 0)
                break;
            // If the blocking time is not zero and the CTL value does not change during this period, it indicates that
            // During this time, if no new task is submitted internally or externally, the current thread will be destroyed
            if (parkTime != 0L && ctl == c &&
                deadline - System.nanoTime() <= 0L &&
                U.compareAndSwapLong(this, CTL, c, prevctl))
                return false;                     // shrink pool
        }
    }
    return true;
}

Let's talk about the runTask() method of thread executing tasks. In this method, the status will be modified to the execution status first. After the stolen tasks are executed, the tasks in their own work queue will be executed. When executing their own tasks, unless FIFO mode is specified, the LIFO mode will be used by default. After all tasks are executed, the status will be changed to the scanning state again. The overall logic is relatively simple.

Let's talk about the hang / block method awaitWork. When the thread can't scan the task, it will first check whether it needs to spin. If necessary, it will use random seeds to realize random spin. After the spin, if the number of suspended (idle) threads in the pool is too large, or the external has not submitted a new task for a long time, the threads will be destroyed directly, so as to reduce the number of threads.

The implementation of thread destruction is also interesting, as shown in the previous section< Thread pool analysis >It is learned that the thread Reuse Principle in the thread pool actually blocks the run() method by means of an endless loop and does not let the run() method end, so that the thread will not stop. In this method, when it is necessary to reduce the number of threads, it will directly return false to let the loop in the external runWorker() method exit, resulting in the end of run() and the normal execution termination of threads, so as to reduce the number of threads.

2, Analysis on the implementation process of task splitting and merging

When analyzing the composition of Fork/Join framework members, I briefly mentioned the fork/join() method. Next, I will break down its implementation process in detail and refer to it first< Part I >Clips in:

// ForkJoinTask class → fork method
public final ForkJoinTask<V> fork() {
    Thread t;
    // Judge whether the currently executing thread is a worker thread in the pool
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        // If yes, the task is directly pushed into the task queue of the current thread
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        // If not, it is pushed into the task queue of a worker thread in the common pool
        ForkJoinPool.common.externalPush(this);
    // Returns the current ForkJoinTask object to facilitate recursive splitting
    return this;
}

// ForkJoinTask class → join method
public final V join() {
    int s;
    // Judge whether the task execution status is abnormal end status
    if ((s = doJoin() & DONE_MASK) != NORMAL)
        // Throw relevant exception stack information
        reportException(s);
    // After normal execution, the execution result is returned
    return getRawResult();
}
// ForkJoinTask class → doJoin method
private int doJoin() {
    int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
    // If status < 0, the status value will be returned directly
    return (s = status) < 0 ? s :
      // Judge whether the current thread is a worker thread in the pool
        ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
        // If yes, take out the current task execution in the thread task queue, and return the status value after execution
        (w = (wt = (ForkJoinWorkerThread)t).workQueue).
        // Try to empty the top of the stack task and then execute the task
        tryUnpush(this) && (s = doExec()) < 0 ? s :
        // If the execution is not completed, call the awaitJoin method to wait for the execution to complete
        wt.pool.awaitJoin(w, this, 0L) :
      // If not, call the externalAwaitDone() method to block and suspend the current thread
      // The task is executed by the common thread pool
        externalAwaitDone();
}
  • fork method logic:
    • ① Judge whether the current thread is a worker thread type in the pool
      • Yes: press the current task into the task queue of the current thread
      • No: press the current task into the task queue of a worker thread in the common pool
    • ② Returns the current ForkJoinTask task task object to facilitate recursive splitting
  • Dojoin & join method logic:
    • ① Judge whether the task status is less than 0:
      • Less than: indicates that the task has ended and returns the status value
      • Not less than: judge whether the current thread is a worker thread in the pool:
        • Yes: try to fetch the current task from the top / end of the stack to execute:
          • Task at the top of the stack: execute the task and return the status value at the end of execution
          • Not at the top of the stack: call the awaitJoin method and wait for execution to end
        • No: call the externalAwaitDone() method to block and suspend the current thread and wait for the end of task execution
    • ② Judge whether the task execution status is abnormal end status. If yes, throw exception stack information
      • If the task status is cancelled, a cancelationexception is thrown
      • If the task status is abnormal end, the corresponding execution exception information is thrown
    • ③ If status is in normal end status, the execution result will be returned directly

The code of doJoin method may seem difficult to understand. It is the same as the way of "understanding odd index calculation when analyzing the principle of worker thread registration" in the previous article. Write it yourself and understand it. Another way is as follows:

private int doJoin() {
    int s; Thread t; ForkJoinWorkerThread wt; 
    ForkJoinPool.WorkQueue w;
    // If the task has been completed, return to the task status directly
    if ((s = status) < 0) {
        return s;
    }
    t = Thread.currentThread();
    boolean isForkJoinThread = t instanceof ForkJoinWorkerThread;
    // If the current thread is not a worker thread, that is, the external thread directly calls the join method to merge
    if (!isForkJoinThread) {
        // Wait for the task to be executed by the thread pool allocated thread and return to the task status
        return externalAwaitDone();
    }
    // If the current thread is a worker thread
    wt = (ForkJoinWorkerThread) t;
    w = wt.workQueue;
    // If the current task is at the end of the queue / top of the stack, it will pop up directly
    if (w.tryUnpush(this)) {
        // Then perform the task that pops up
        return this.doExec();
    }
    // If the current task is not at the end of the queue / top of the stack, awaitJoin is called to wait
    return wt.pool.awaitJoin(w, this, 0L);
}

In this way, you can clearly see the logic of the doJoin method. Then go on to analyze. In fact, the principle and implementation of fork is relatively simple. The following focuses on the implementation of join. First look at the tryUnpush() method:

// ForkJoinTask class → tryUnpush() method
final boolean tryUnpush(ForkJoinTask<?> t) {
    ForkJoinTask<?>[] a; int s;
    // Try to set the top of stack / end of Queue task to null. If t is the top of stack task in the queue, try to set cas to null
    if ((a = array) != null && (s = top) != base &&
        U.compareAndSwapObject
        (a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
        U.putOrderedInt(this, QTOP, s);
        return true;
    }
    return false;
}

When the worker thread merges the results, if the task is forked to the top of the stack / end of the queue, execute the task and return. However, if it is not at the top of the stack, it may be pressed down by tasks from other forks or stolen by other threads, then it will enter the awaitJoin() method.

2.1 awaitJoin method

Next, let's take a look at the awaitJoin() method. The source code is as follows:

// ForkJoinTask class → awaitJoin() method
final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
    int s = 0;
    if (task != null && w != null) {
        // Record the previous task being merged
        ForkJoinTask<?> prevJoin = w.currentJoin;
        // Record join merge current task
        U.putOrderedObject(w, QCURRENTJOIN, task);
        // CountedCompleter is a subclass implementation of ForkJoinTask
        CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
            (CountedCompleter<?>)task : null;
        // Spin operation
        for (;;) {
            // 1. The task has been executed. You don't need to spin any more. Return directly
            if ((s = task.status) < 0)
                break;
            // If the task is of type CountedCompleter, get its derived subtask execution
            if (cc != null)
                helpComplete(w, cc, 0);
            // If the queue is not empty, try to get the task execution that needs to be join ed from the queue.
            // If the current queue task is empty, it indicates that the current task has been stolen by other worker threads
            // tryRemoveAndExec is used to attempt to execute the current task stored in the queue,
            // If the task of the current join is not found in the queue, it means that it has been stolen by other threads
            else if (w.base == w.top || w.tryRemoveAndExec(task))
                // Find the worker thread that steals the join task and help the stealer execute the stealer's task
                helpStealer(w, task);
            
            // 3. Judge whether the task has been executed again, and exit after execution
            // If the task is stolen and can be executed to this step, it must be the previous one
            // The helpsteeler method exited for two reasons:
            //      1. The tasks you need to join and merge have been completed
            //      2. The stealing chain is broken or there are no tasks to steal. Prepare to block
            if ((s = task.status) < 0)
                break;
            long ms, ns;
            if (deadline == 0L)
                ms = 0L;
            else if ((ns = deadline - System.nanoTime()) <= 0L)
                break;
            else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
                ms = 1L;
            // 4. Call the tryCompensate method to compensate the thread pool
            // Before blocking, in order to prevent all threads in the thread pool from blocking,
            // An active thread (wake-up or new) is compensated for the thread pool
            if (tryCompensate(w)) {
                // Spin and block, waiting for other threads to complete the stolen join task
                task.internalWait(ms);
                // Number of active threads superimposed after wake-up
                U.getAndAddLong(this, CTL, AC_UNIT);
            }
        }
        // When the task is completed, restore the currentJoin to the previous currentJoin value
        U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
    }
    return s;
}
// ForkJoinTask class → tryRemoveAndExec() method
final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; int m, s, b, n;
    // If the task array in the queue is not empty and has been initialized
    if ((a = array) != null && (m = a.length - 1) >= 0 &&
        task != null) {
        // Is there a task in the queue
        while ((n = (s = top) - (b = base)) > 0) {
            for (ForkJoinTask<?> t;;) {     // traverse from s to b
                // Take values from the top of the stack to the bottom
                long j = ((--s & m) << ASHIFT) + ABASE;
                // Because of concurrency, tasks may be stolen by thieves
                if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                    // If task stealing occurs, it means that the s has been executed to the bottom of the stack
                    // If the stolen join task was stolen at the top of the stack, true will be returned
                    return s + 1 == top;     // shorter than expected
                // If the task is found
                else if (t == task) {
                    boolean removed = false;
                    // The current join task is at the top of the stack. Try to pop it up
                    // If cas fails, it means that it is stolen by other threads, and the queue is empty
                    if (s + 1 == top) {      // pop
                        if (U.compareAndSwapObject(a, j, task, null)) {
                            U.putOrderedInt(this, QTOP, s);
                            removed = true;
                        }
                    }
                    // The current join task is not at the top of the stack and the bottom of the stack has not changed,
                    // Replace the pit of the current join task with the EmptyTask object
                    else if (base == b)      // replace with proxy
                        // Because the task is not at the top of the stack, it cannot be directly replaced with null,
                        // If you replace it with null, you must move the pointer. Obviously, you can't move the pointer here
                        // In many places, null is used as concurrency judgment,
                        // When other worker threads get null,
                        // It will be considered that the task has been stolen by other threads,
                        // So you'll never get the task
                        removed = U.compareAndSwapObject(
                            a, j, task, new EmptyTask());
                    if (removed)
                        //Perform tasks
                        task.doExec();
                    break;
                }
                // If other tasks have been completed and are tasks at the top of the stack, set it to blank
                else if (t.status < 0 && s + 1 == top) {
                    if (U.compareAndSwapObject(a, j, t, null))
                        U.putOrderedInt(this, QTOP, s);
                    break;                  // was cancelled
                }
                // If it is not found at the top or bottom of the stack, false is returned
                // Although the mission was stolen, I didn't participate in help Steeler
                if (--n == 0)
                    return false;
            }
            // The task has been completed
            if (task.status < 0)
                return false;
        }
    }
    return true;
}

The overall logic of awaitJoin method is relatively simple, as follows:

  • ① Check whether the work queue of the current thread is empty
    • Null: indicates that the task has been stolen
    • Not null: use tryRemoveAndExec to find the tasks that need to be join ed in the whole queue
      • Found: execute task
      • Not found: the representative task is still stolen (in this case, it does not participate in the helpsteeler method)
  • ② If the task is stolen, find the thief through helpsteeler and help it execute the task
  • ③ If you exit from the helpsteeler method, the task is checked again for completion:
    • Executed end: exit the loop and merge the results
    • Unexecuted end: prepare to enter blocking to avoid waste of CPU resources
  • ④ Before blocking, the thread pool will be compensated, because the current thread may be the last active thread in the thread pool. In order to avoid all threads in the thread pool "dying", an active thread will be compensated for the thread pool first

ok ~, let's take a look at the logic of the tryRemoveAndExec method, as follows:

  • Determine whether there are tasks in the queue:
    • Does not exist: returns true, and the external awaitJoin method enters the helpsteeler logic
    • Exist: judge whether the task is at the end of the queue / at the bottom of the stack:
      • Try to pop up the top of the stack task in CAS:
        • Success: execute task
        • Failure: it represents CAS failure. The task is stolen by another thread and enters the helpsteeler logic
      • No: it may be pushed down by other tasks. Start from the top of the stack to find the whole queue:
        • Found: replace the task with an EmptyTask object and execute the task
        • Not found: it means that the task has been stolen, but although it has not been found, it will not participate in helpsteeler, but it will check whether the task is completed again before exiting

The tryRemoveAndExec method is relatively simple. It is mainly used to traverse the WorkQueue of the current thread and find the task execution to join in the queue. During execution, if the queue is empty, the task is at the top of the stack but cas fails, and the task to be joined is not found after traversing the whole queue, these three cases represent that the task has been stolen. In the first two cases, the helpsteeler will be entered to help the thief execute the task, while in the last case, the blocking will be directly exited (personal guess: it may be that traversing the entire queue will lead to a period of time. The stolen task is likely to have been completed or almost completed within this period of time. Therefore, instead of helping the thief to perform the task, it is better to block and wait for a while).

2.2. Helpsteeler helps the thief execute the method

Let's take a look at the helpsteeler method. The source code is as follows:

// ForkJoinTask class → helpsteeler() method
private void helpStealer(WorkQueue w, ForkJoinTask<?> task) {
    WorkQueue[] ws = workQueues;
    int oldSum = 0, checkSum, m;
    // If the queue array and task queue are not empty
    if (ws != null && (m = ws.length - 1) >= 0 && w != null &&
        task != null) {
        do {                    // restart point
            checkSum = 0;      // for stability check
            ForkJoinTask<?> subtask;
            WorkQueue j = w, v;   // v is subtask stealer
            descent: for (subtask = task; subtask.status >= 0; ) {
                // j.hint starts with the random value of the j queue in the queue group used to calculate the subscript,
                // If the thief is found, this value will become the subscript of the corresponding thief
                // j.hint | 1 = an odd number, k += 2: step size is 2, odd + 2 = odd number
                for (int h = j.hint | 1, k = 0, i; ; k += 2) {
                    // Find all odd digits of a complete corresponding array,
                    // If the task is still not found, exit directly (the execution may be completed)
                    if (k > m)        // can't find stealer
                        break descent;
                    //(H + k) & M: calculate the odd subscript in an array,
                    // Check whether the subscript queue has stolen its own task
                    if ((v = ws[i = (h + k) & m]) != null) {
                        // Determine whether currentsteel is the current task
                        if (v.currentSteal == subtask) {
                            // Yes, record the index of the thief in the queue group
                            j.hint = i;
                            break;
                        }
                        // After checking a queue, the checksum is calculated
                        checkSum += v.base;
                    }
                }
                // The current thread performs tasks for the thief thread
                for (;;) {           // help v or descend
                    ForkJoinTask<?>[] a; int b;
                    // The bottom of the thief thread queue stack is also included in the checksum because it steals the task
                    // , it is possible to fork out smaller tasks and steal them by other threads
                    checkSum += (b = v.base);
                    // Get the task that the thief is currently join ing
                    ForkJoinTask<?> next = v.currentJoin;
                    //Subtask. Status < 0 task execution completed
                    // If task execution results
                    //      Or the task to be merged by the worker thread is no longer a subtask
                    //      Or the task stolen by the thief is no longer the current join task
                    // Then exit the loop
                    if (subtask.status < 0 || j.currentJoin != subtask ||
                        v.currentSteal != subtask) // stale
                        break descent;
                    // If there are no tasks in the current thread, it will help it join and merge tasks
                    // The subtask will be re assigned here. If it is empty, it will return to descent
                    // Loop to the next iteration
                    if (b - v.top >= 0 || (a = v.array) == null) {
                        // If the stealer does not need to join merge tasks,
                        // Exit to judge whether the task is over
                        if ((subtask = next) == null)
                            break descent;
                        // If the thief has a task to join,
                        // That will help the thief find the thief who stole its mission
                        j = v;
                        break;
                    }
                    // If there are tasks in the thief's queue, start the thief from the bottom of the stack / head of the queue
                    // Thread's task execution (may steal its own stolen task)
                    // The subtasks from fork),
                    int i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                    ForkJoinTask<?> t = ((ForkJoinTask<?>)
                                         U.getObjectVolatile(a, i));
                    // After stealing, check whether the bottom of the stack / team head has changed,
                    // If it changes, it means that other threads are stealing the tasks of the reader thread,
                    // Avoid invalid cas and steal a new task directly
                    if (v.base == b) {
                        // ==null indicates that the task was stolen by another thread,
                        // Then the assignment is null, but the base has not been updated in time
                        if (t == null)             // stale
                            // Go back to the descent flag for the next iteration
                            break descent;
                        // If there is no change, cas will empty the task of stack bottom / team head
                        // This can tell other threads that the current task has been stolen
                        if (U.compareAndSwapObject(a, i, t, null)) {
                            // Update stack bottom pointer
                            v.base = b + 1;
                            // Record your last stolen task
                            ForkJoinTask<?> ps = w.currentSteal;
                            int top = w.top;
                            do {
                                // Update the newly stolen task to currentsteel
                                U.putOrderedObject(w, QCURRENTSTEAL, t);
                                // Perform the stolen task
                                t.doExec();  // clear local tasks too
                                // When the task of the join has not been completed,
                                // And the task just executed has a fork task,
                                // Then w.top! = top will be established,
                                // At this point, you have to w.pop() to execute the local task
                            } while (task.status >= 0 &&
                                     w.top != top &&
                                     (t = w.pop()) != null);
                            // Restore the original stolen record after execution
                            U.putOrderedObject(w, QCURRENTSTEAL, ps);
                            // Then check whether there are tasks in your queue
                            // If w.base! = w.top is established, it represents its own queue
                            // At this time, it will end directly and go back to perform its own task,
                            // There is no need to help other threads perform tasks
                            if (w.base != w.top)
                                return;     // can't further help
                        }
                    }
                }
            }
            // There are two conditions for exiting helpsteeler:
            // 1. After the join task you need to merge has been completed, go back to perform your own merge task;
            // 2. Your join task has not been completed, but you can't steal the task. Then exit blocking
            //    The current thread, because continuing to find it is also an idle run, wasting CPU resources
        } while (task.status >= 0 && oldSum != (oldSum = checkSum));
    }
}

This method is the core embodiment of the "work stealing idea" implemented by ForkJoin framework. It completes the "work stealing" of the whole framework together with scan scanning method Implementation. In the runTask method after the scan method, a value will be assigned to currentsteel, and the helpsteeler method relies on a stealing chain formed by the member and the currentJoin member to help the thief perform tasks. The specific logic of helpsteeler is no longer analyzed. You can refer to the notes in the above source code.

In short, the core idea of the helpsteeler method is to help execute and help the thief execute its tasks, but it will not only help the thief execute, but also help the thief execute, help the thief execute, and help the thief execute tasks based on the stealing chain formed by currentsteel and currentJoin members. The previous example Understand as follows:

  • ① Thread T1 needs to join task a, but task a is stolen. It starts to traverse and find all odd queues
  • ② After searching, it is found that TaskA = = thread t2.currentsteel member. At this time, T2 is the thief of T1
  • ③ T1 steals a task execution from the bottom of T2 queue stack, steals another execution after execution, and continues to steal
  • ④ T1 finds that there are no tasks in T2's queue, and T1 will continue to look for the thread that stole t2.currentjoin
  • ⑤ After traversal, it is found that T2. Currentjoin = = T5. Currentsteel, T5 is the thief of T2
  • ⑥ Then T1 continues to steal a task from the bottom of the T5 queue, and continues to steal after completion
  • ⑦ T1 finds that there are no tasks in T5's queue. T1 will continue to look for those who stole T5. Currentjoin
  • ⑧ According to the theft chain, it goes on like this

Through the above process, it can be found that T1. currentJoin → T2. Currentsteel → T2. currentJoin → T5. Currentsteel → T5. currentJoin... Form a theft chain through two members of currentsteel and currentJoin. If you understand the link relationship, you also understand the helpsteeler method. However, it is worth noting that when will the helpsteeler method exit? The answer is: the theft chain will quit when it breaks. There are three situations that can cause the theft chain to break:

  • ① Currentsteel or currentJoin of any worker thread is null
  • ② The currentsteel or currentJoin of any worker thread has been executed
  • ③ The join task of the current thread has been completed

In fact, in the final analysis, the helpsteeler method is an implementation point to optimize the performance of the ForkJoin framework. The core point is to reduce thread blocking due to merging and help other threads execute a task while waiting for the join task to execute. This ensures that each thread does not stop working and can also speed up the processing speed of the overall framework. At the same time, during the help execution period, The stolen join task is finished.

2.3 tryCompensate method for compensating active threads

Let's look at the tryCompensate method for compensating active threads for the thread pool:

// ForkJoinPool class → tryCompensate() method
private boolean tryCompensate(WorkQueue w) {
    boolean canBlock;
    WorkQueue[] ws; long c; int m, pc, sp;
    // If the thread pool has been stopped and is in the terminate state, it cannot and does not need to be blocked
    if (w == null || w.qlock < 0 ||           // caller terminating
        (ws = workQueues) == null || (m = ws.length - 1) <= 0 ||
        (pc = config & SMASK) == 0)           // parallelism disabled
        canBlock = false;
    // If there is a suspended idle thread in the lower 32 bits of ctl, try to wake it up, and block yourself if successful
    // After waking up, you may perform the subtask of your stolen task fork to a certain extent
    // The second parameter of tryRelease is 0. When the wake-up succeeds, it means that the current thread will be blocked,
    // New idle threads are awakened, so there is no need to reduce the number of active threads first and then add
    else if ((sp = (int)(c = ctl)) != 0)      // release idle worker
        canBlock = tryRelease(c, ws[sp & m], 0L);
    // If there are no idle threads, create a new thread
    // This will cause the number of threads in the thread pool to exceed the number of parallel threads specified during creation over a period of time
    else {
        // Gets the number of active threads in the pool
        int ac = (int)(c >> AC_SHIFT) + pc;
        // Gets the number of bus passes in the pool
        int tc = (short)(c >> TC_SHIFT) + pc;
        int nbusy = 0;       // validate saturation
        for (int i = 0; i <= m; ++i) {  // two passes of odd indices
            WorkQueue v;
            // Find the queue with odd positions and cycle m times, that is, execute it twice.
            // Why twice? Mainly to judge the stability, it may be the second time
            //  The number of active threads executing the task becomes less when the
            if ((v = ws[((i << 1) | 1) & m]) != null) {
                // Check if the worker thread is processing a task,
                // If you are not processing a task, it means you are idle. You can get other tasks to execute
                if ((v.scanState & SCANNING) != 0)
                    break;
                ++nbusy;
            }
        }
        // If the thread pool state is unstable, the current thread cannot be suspended
        // If nbusy= TC * 2 indicates that there are still idle or scanning worker threads
        // If ctl= C represents that the ctl has changed. It is possible that after the thread completes the task,
        // It is not scanned that a new task is inactivated. In this case, do not hang up and spin for a period of time
        if (nbusy != (tc << 1) || ctl != c)
            canBlock = false;         // unstable or stale
        
        // tc: number of bus processes in the pool pc: number of parallels ac: number of active threads in the pool
        // TC > = PC means that there are enough threads at this time. Of course, it does not mean that new threads will not be created
        // AC > 1 means that there are other active threads besides themselves
        // w.isEmpty() the current worker queue is empty and there are no tasks to execute
        // If the above three conditions are met, it can be blocked directly without compensation
        else if (tc >= pc && ac > 1 && w.isEmpty()) {
            long nc = ((AC_MASK & (c - AC_UNIT)) |
                       (~AC_MASK & c));      // uncompensated
            //cas ctl
            canBlock = U.compareAndSwapLong(this, CTL, c, nc);
        }
        // This is a special handling of the commonPool common thread pool
        // If the number of bus passes exceeds MAX_CAP will throw an exception
        else if (tc >= MAX_CAP ||
                 (this == common && tc >= pc + commonMaxSpares))
            throw new RejectedExecutionException(
                "Thread limit exceeded replacing blocked worker");
        else {                                // similar to tryAddWorker
            boolean add = false; int rs;      // CAS within lock
            // Prepare to create a new working thread (only the number of bus processes is added here, not the number of active threads)
            //      Because the current worker thread will block after the compensation thread is successfully created
            // However, this will cause the number of bus passes to exceed the number of parallels
            long nc = ((AC_MASK & c) |
                       (TC_MASK & (c + TC_UNIT)));
            // New worker threads can be created only when the thread pool is not stopped
            if (((rs = lockRunState()) & STOP) == 0)
                add = U.compareAndSwapLong(this, CTL, c, nc);
            unlockRunState(rs, rs & ~RSLOCK);
            // Create a new worker thread
            canBlock = add && createWorker(); // throws on exception
        }
    }
    return canBlock;
}

The logic in this method is also relatively simple:

  • ① Judge whether there is a suspended idle thread in the pool. If so, wake it up to replace itself
  • ② If there are no suspended idle threads, judge whether there are two or more active threads in the pool, whether the number of bus processes is saturated, and whether their own work queue is empty. If all these are met, there is no need to compensate and suspend directly
  • ③ If the above three conditions are not met, judge whether the number of threads is closed. If not, create a new thread compensation

It is worth mentioning that the tryCompensate method will cause the number of bus processes in the pool to exceed the number of parallelism specified when creating the thread pool for a period of time. And if you use the Fork/Join framework, if you call the method of submission in ForkJoinTask: sumbit()/invoke()/execute(), it will cause the thread pool to compensate the thread all the time, and if hardware allows, it will cause compensation to create the maximum 0x7fff = 32767 threads.

2.4 external awaitdone method

The analysis of doJoin logic mentioned earlier: if an external thread calls the join method, it will call the externalAwaitDone method. Then take a look at this method:

// ForkJoinPool class → externalAwaitDone() method
private int externalAwaitDone() {
    // If the task is of CountedCompleter type, try to use the common pool for external help execution,
    // After execution is completed, return the completed task status
    int s = ((this instanceof CountedCompleter) ? // try helping
             ForkJoinPool.common.externalHelpComplete(
                 (CountedCompleter<?>)this, 0) :
                 // The current task is not a CountedCompleter. Try to get the current task from the top of the stack
                 // The join task is executed by the common pool. If it is not at the top of the stack, s becomes 0
             ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
    // If s > = 0, it means that the task is in an unfinished state and needs to be blocked
    if (s >= 0 && (s = status) >= 0) {
        boolean interrupted = false;
        do {
            // First set the SIGNAL flag to notify other threads that they need to be awakened
            if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                // Suspend the thread through synchronized.wait()
                synchronized (this) {
                    if (status >= 0) { // Double detection
                        try {
                            wait(0L);   // Suspend thread
                        } catch (InterruptedException ie) {
                            interrupted = true;
                        }
                    }
                    else
                        // If it is found that it has completed, wake up all waiting threads
                        notifyAll();
                }
            }
        // If the task is not completed, it will loop all the time
        } while ((s = status) >= 0);
        // Response interrupt operation
        if (interrupted)
            Thread.currentThread().interrupt();
    }
    // Return to execution status after execution
    return s;
}

The externalAwaitDone method is the simplest. If the task is at the top of the stack, it will pop up for execution directly. If it is not, the current thread will be suspended until the task execution ends and other threads wake up.

2.5 summary of the principle of task splitting and merging

The task fork operation is relatively simple. You only need to push the split task into your own work queue. For the task result merging: join operation, the implementation is slightly complicated. The general idea is to first find the task that needs to be joined in your queue, and if it is found, execute it and merge the results. If it is not found, it is stolen. You need to find the thief thread, and will always help the thief execute the task according to the stealing chain before the execution of the join task is completed. If the stealing chain is broken but the join task is not completed, suspend the current working thread, However, before suspending, it will be decided according to the situation whether to compensate an active thread for the thread pool to work instead of itself, so as to prevent all threads of the whole thread pool from blocking and generate the "suspended" state of the thread pool. Of course, if it is a join operation executed by an external thread, if the task to be joined has not been completed, it needs to be handed over to the commonPool public pool for processing.

3, Implementation principle of task cancellation in ForkJoin

The cancel method of task cancellation is implemented on the Future interface. The logic is relatively simple. The source code is as follows:

// ForkJoinTask class → cancel() method
public boolean cancel(boolean mayInterruptIfRunning) {
    // When trying to change the task status to CANCELLED, it returns true for success and false for failure
    return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
}

// ForkJoinTask class → setCompletion() method
private int setCompletion(int completion) {
    // Turn on spin (dead cycle)
    for (int s;;) {
        // If the task has been completed, the status after execution will be returned directly
        if ((s = status) < 0)
            return s;
        // If not, try to modify the status to input parameter: completion status through cas mechanism
        if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
            if ((s >>> 16) != 0)
                synchronized (this) { notifyAll(); }
            return completion;
        }
    }
}

The logic of canceling a task is relatively simple. Task cancellation can only occur when the task has not been executed. If the task has been completed, it will directly return to the execution status. If the task has not been executed, it will try to use the spin + CAS mechanism to modify the task status to CANCELLED status. Success means that the task is CANCELLED successfully.

4, Implementation of closing ForkJoinPool thread pool

Generally, when the thread pool is normally closed, the thread pool will be stopped through the shundown method, and then analyze the implementation of thread pool closing:

// ForkJoinPool class → shutdown() method
public void shutdown() {
    // Check permissions
    checkPermission();
    // Close thread pool
    tryTerminate(false, true);
}

// ForkJoinPool class → checkPermission() method
private static void checkPermission() {
    // Get rights manager
    SecurityManager security = System.getSecurityManager();
    // Check whether the current thread has the permission to close the thread pool
    if (security != null)
        security.checkPermission(modifyThreadPermission);
}

// ForkJoinPool class → tryTerminate() method
private boolean tryTerminate(boolean now, boolean enable) {
    int rs;
    // If it is a common public pool, it cannot be closed. The closing of common is bound to the Java program
    if (this == common)            // cannot shut down
        return false;
    // If the thread pool is still running, check whether enable is true. If false, exit
    if ((rs = runState) >= 0) {
        if (!enable)
            return false;
        rs = lockRunState();                  // enter SHUTDOWN phase
        // If the thread pool is to be closed, first change the running state to the SHUTDOWN flag
        unlockRunState(rs, (rs & ~RSLOCK) | SHUTDOWN);
    }
    // If the thread pool is not in stop state (RS & stop = = 1 means it is in stop state)
    if ((rs & STOP) == 0) {
        // If the now input parameter is false, the following logic will be entered
        if (!now) {                 // check quiescence
            // Traverse the entire work queue array
            for (long oldSum = 0L;;) {        // repeat until stable
                WorkQueue[] ws; WorkQueue w; int m, b; long c;
                // Taking the current ctl value as the initial validity and
                long checkSum = ctl;
                // Detect the number of active threads in the pool. If > 0, it cannot be directly set to stop status
                if ((int)(checkSum >> AC_SHIFT) + (config & SMASK) > 0)
                    return false;             // still active workers
                // If the work queue is completely logged off, it can be set to stop status
                if ((ws = workQueues) == null || (m = ws.length - 1) <= 0)
                    break;                    // check queues
                // Open cycle
                for (int i = 0; i <= m; ++i) {
                    // Cycle through each work queue
                    if ((w = ws[i]) != null) {
                        // If there are still tasks in the queue and the current queue is active
                        if ((b = w.base) != w.top || w.scanState >= 0 ||
                            w.currentSteal != null) {
                            // Wake up idle threads to help execute unprocessed tasks
                            tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                            return false;     // arrange for recheck
                        }
                        // Take the bottom of the stack as the checksum
                        checkSum += b;
                        // Cancel all tasks in even digit queue (externally submitted tasks)
                        if ((i & 1) == 0)
                            w.qlock = -1;     // try to disable external
                    }
                }
                // After looping the array twice, the validation and are consistent, which means that the tasks are empty,
                // At the same time, no new thread has been created, so you can set the stop state
                if (oldSum == (oldSum = checkSum))
                    break;
            }
        }
        // If the thread pool has not stopped, it is set to stop state
        if ((runState & STOP) == 0) {
            rs = lockRunState();              // enter STOP phase
            unlockRunState(rs, (rs & ~RSLOCK) | STOP);
        }
    }
    
    int pass = 0;                             // 3 passes to help terminate
    for (long oldSum = 0L;;) {                // or until done or stable
        WorkQueue[] ws; WorkQueue w; ForkJoinWorkerThread wt; int m;
        long checkSum = ctl;
        // All queues are empty or all threads are logged off
        if ((short)(checkSum >>> TC_SHIFT) + (config & SMASK) <= 0 ||
            (ws = workQueues) == null || (m = ws.length - 1) <= 0) {
            // If the thread pool is not TERMINATED
            if ((runState & TERMINATED) == 0) {
                rs = lockRunState();          // done
                // First change the thread pool state to TERMINATED state
                unlockRunState(rs, (rs & ~RSLOCK) | TERMINATED);
                synchronized (this) { notifyAll(); } // for awaitTermination
            }
            break;
        }
        // Open cycle
        for (int i = 0; i <= m; ++i) {
            // Process each queue
            if ((w = ws[i]) != null) {
                checkSum += w.base;
                w.qlock = -1;                 // try to disable
                if (pass > 0) {
                    // Cancel all tasks in each queue
                    w.cancelAll();            // clear queue
                    // Interrupt the execution thread and wake up all suspended threads
                    if (pass > 1 && (wt = w.owner) != null) {
                        if (!wt.isInterrupted()) {
                            try {             // unblock join
                                wt.interrupt();
                            } catch (Throwable ignore) {
                            }
                        }
                        if (w.scanState < 0)
                            U.unpark(wt);     // wake up
                    }
                }
            }
        }
        // If the two validation sums are inconsistent, assign the last validation sum
        if (checkSum != oldSum) {             // unstable
            oldSum = checkSum;
            pass = 0;
        }
        // The thread pool state is stable
        // All tasks are cancelled, the execution thread is interrupted, the suspended thread is awakened and interrupted
        else if (pass > 3 && pass > m)        // can't further help
            break;
        // If a thread is suspended due to inactivation
        else if (++pass > 1) {        // try to dequeue
            long c; int j = 0, sp;    // bound attempts
            // Wake up all threads according to the blocking chain recorded in ctl
            while (j++ <= m && (sp = (int)(c = ctl)) != 0)
                tryRelease(c, ws[sp & m], AC_UNIT);
        }
    }
    return true;
}

The implementation logic of thread pool closing is also relatively simple. First, the thread pool will be marked as SHUTDOWN state, and then the next step will be processed according to the situation. If there are no active threads in the thread pool and there are not many tasks, change the state to STOP state, and four things will be processed in the STOP state:

  • ① Change the status of all active queues to logout status, w.qlock=-1
  • ② Cancels all outstanding tasks in the entire thread pool
  • ③ Wake up all threads suspended from blocking due to deactivation
  • ④ Try to interrupt all active threads executing, wake up the threads with scanstate < 0, and ensure that some threads that have not been suspended can also be interrupted

Finally, when all threads are interrupted and the unexecuted tasks are cancelled, the state will be changed to TERMINATED and the thread pool will be closed.

5, Summary

The ForkJoin branch merge framework is almost the most difficult part of the source code of the entire JUC package, because the whole framework is relatively large and complex to analyze. So far, ManagedBlocker and CoutedCompleter have not been analyzed. Because the analysis of ForkJoin framework is too long, I won't repeat these two. However, those interested in CoutedCompleter can refer to: CoutedCompleter analysis In this article, its role is more to provide services for Stream parallel streams in Java 8. ManagedBlocker supports the ForkJoin framework to handle blocking tasks.

In general, the ForkJoin branch and merge framework idea is very excellent, which completely implements the divide and conquer and work stealing ideas. Each member of the whole framework performs his own duties but cooperates closely. A queue array is used internally to store internal and external tasks in odd / even bits, and the work and stealing ideas are realized in the way of double ended queues. However, its internal implementation involves a lot of bit operation knowledge, so half monks and small partners who have worked for many years will be a little rusty. It will be difficult to see its source code implementation, but it is enough to understand the general idea. There is no need to stick to the details of any source code analysis knowledge.

Final summary:

  • Create a ForkJoinPool. The number of initialized parallels = the number of cpu logical cores. There are no queues and threads in the pool
  • Submit a task to the external thread pool: pool.submit(task)
  • Initialize queue array, capacity: 2 * Max {parallel number, 2 ^ n}
  • Create a shared queue with a capacity of 2 ^ 13 and randomly place it in an even index bit of the queue array
  • Externally submitted tasks are stored in this shared queue with a location value of 2 ^ 12
  • Create another thread and assign it a queue with a capacity of 2 ^ 13, which is randomly placed in an odd index bit in the array
  • Thread start execution
  • At a random location, the thread starts to traverse all queues from this location, and finally scans the previously submitted tasks and takes them out of the queue
  • The thread executes processing tasks. First, it splits two subtasks
    • If you submit with invokeAll, one subtask is executed and the other is pushed into the queue
    • If you commit with fork, both are pushed into the work queue
  • The submitted subtask triggers the creation of new threads and the allocation of new work queues, which are also placed in odd positions
  • The submitted subtask may still be executed by the current thread, but it may also be stolen by other threads
  • Threads join and merge at subtasks. During the join, it will help the thief process the task and steal its task execution
    • Tasks at the bottom of the priority stealer queue
    • If the stealer queue is empty, the stealer task of the stealer will be found according to the stealer chain
    • If there are no tasks in the whole pool, it will enter blocking. Before blocking, it will compensate the active thread according to the situation
  • No matter which thread executes the submitted subtask, the above split / commit / steal / block steps may still be repeated
  • Only when the task is split thin enough to reach the split threshold can these subtasks be really executed
  • When processing is completed, the results will be returned recursively layer by layer as when splitting tasks
  • Until all subtasks are finished, all subtasks are merged to get the final result
  • If the external task is not submitted again, all threads that cannot be scanned will be inactivated and enter the inactive state
  • When there are no tasks, the thread pool will reduce the number of threads until all threads are destroyed, the queues with odd index bits are logged off, and there is only one queue with even index bits originally created in the ForkJoinPool, so as to accept externally submitted tasks again, and then repeat all steps from the beginning

Topics: Java Multithreading