The introduction of Fork/Join framework can be consulted Fork/Join Framework . There are two core classes of Fork/Join framework: Fork Join Pool, which is mainly responsible for task execution, and Fork Join Task, which is mainly responsible for task splitting and result merging.
ForkJoinPool
Like ThreadPool Executor, it is also a thread pool implementation. It also implements Executor and Executor Service interfaces. The class diagram is as follows:
Core internal class WorkQueue
static final class WorkQueue { // Initial queue capacity static final int INITIAL_QUEUE_CAPACITY = 1 << 13; // Maximum queue capacity static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M volatile int qlock; // 1: locked, < 0: terminate; else 0 // Index Bit of Next Team volatile int base; // index of next slot for poll // Next entry index bit int top; // index of next slot for push // Containers for storing tasks ForkJoinTask<?>[] array; // the elements (initially unallocated) final ForkJoinPool pool; // the containing pool (may be null) // Threads that perform current queue tasks final ForkJoinWorkerThread owner; // owning thread or null if shared volatile Thread parker; // == owner during call to park; else null WorkQueue(ForkJoinPool pool, ForkJoinWorkerThread owner) { this.pool = pool; this.owner = owner; // Place indices in the center of array (that is not yet allocated) base = top = INITIAL_QUEUE_CAPACITY >>> 1; } // Join the team 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(); } } // Initialization Extension ForkJoin Task<?>[] final ForkJoinTask<?>[] growArray() { ForkJoinTask<?>[] oldA = array; int size = oldA != null ? oldA.length << 1 : INITIAL_QUEUE_CAPACITY; if (size > MAXIMUM_QUEUE_CAPACITY) throw new RejectedExecutionException("Queue capacity exceeded"); int oldMask, t, b; ForkJoinTask<?>[] a = array = new ForkJoinTask<?>[size]; if (oldA != null && (oldMask = oldA.length - 1) >= 0 && (t = top) - (b = base) > 0) { int mask = size - 1; do { // emulate poll from old array, push to new array ForkJoinTask<?> x; int oldj = ((b & oldMask) << ASHIFT) + ABASE; int j = ((b & mask) << ASHIFT) + ABASE; x = (ForkJoinTask<?>)U.getObjectVolatile(oldA, oldj); if (x != null && U.compareAndSwapObject(oldA, oldj, x, null)) U.putObjectVolatile(a, j, x); } while (++b != t); } return a; } // Team out final ForkJoinTask<?> poll() { ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t; while ((b = base) - top < 0 && (a = array) != null) { int j = (((a.length - 1) & b) << ASHIFT) + ABASE; t = (ForkJoinTask<?>)U.getObjectVolatile(a, j); if (base == b) { if (t != null) { if (U.compareAndSwapObject(a, j, t, null)) { base = b + 1; return t; } } else if (b + 1 == top) // now empty break; } } return null; } }
The main function of WorkQueue is to accept tasks submitted externally and to support job theft.
- Each WorkQueue corresponds to a ForkJoin Worker Thread to perform tasks in the queue.
- A ForkJoinTask <?>[] array is used to store tasks. The initialization length of this array is INITIAL_QUEUE_CAPACITY = 1 << 13 = 8192, and the maximum length is MAXIMUM_QUEUE_CAPACITY = 1 << 26 = 67108864. ForkJoinTask <?> [] is initialized at the time of the first submission of the task.
- base and top are used to record the index bits of the next team and the index bits of the next team.
Core attributes
// Instance fields // Thread pool control bit volatile long ctl; // main pool control // Thread pool status volatile int runState; // lockable status // Work queue array volatile WorkQueue[] workQueues; // main registry
Constructor
private ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, int mode, String workerNamePrefix) { this.workerNamePrefix = workerNamePrefix; this.factory = factory; this.ueh = handler; this.config = (parallelism & SMASK) | mode; long np = (long)(-parallelism); // offset ctl counts this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK); }
- int parallelism: Concurrency, default CPU core number
- ForkJoin Worker ThreadFactory: Factory class for creating workthreads
- Uncaught Exception Handler: Strategy for exception handling
- int mode: Queue algorithm tag, 0: FIFO, 65536: FIFO
- String worker Name Prefix: Workthread name prefix
## Core approach
execute()
public void execute(ForkJoinTask<?> task) { if (task == null) throw new NullPointerException(); externalPush(task); }
Just judge whether the task is null, and then call the externalPush method.
externalPush()
final void externalPush(ForkJoinTask<?> task) { // ws work queue array; q: the work queue where the current task is stored; m: the last index bit of the work queue array WorkQueue[] ws; WorkQueue q; int m; // Get a random number int r = ThreadLocalRandom.getProbe(); int rs = runState; if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 && // Find the work queue q that holds the current task (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 && // Get locks on q queues U.compareAndSwapInt(q, QLOCK, 0, 1)) { // A n array of tasks in a:q queue; am is the length of the array; n: the number of tasks in the array; q: the index bit of the next entry ForkJoinTask<?>[] a; int am, n, s; if ((a = q.array) != null && // Determine if the queue is full (am = a.length - 1) > (n = (s = q.top) - q.base)) { // Computing the index bit of the storage task int j = ((am & s) << ASHIFT) + ABASE; // Storage task U.putOrderedObject(a, j, task); // Update top pointer (index bit) U.putOrderedInt(q, QTOP, s + 1); // Unlock U.putIntVolatile(q, QLOCK, 0); if (n <= 1) // Trying to create or activate threads signalWork(ws, q); return; } U.compareAndSwapInt(q, QLOCK, 1, 0); } // The full version of push handles some unusual situations, such as initializing the work queue array workQueues and new work queue workQueues[i] externalSubmit(task); }
- First, determine whether the work queue array is NULL or not. If you go directly to the following full version of push method
- Based on the random number, find the work queue p=workQueues[i] that the current task needs to put in
- If p is NULL, go directly to the following full version of push method
- Get locks on q queues
- Judging whether the queue is full or not, if you go directly to the next full version of the push method
- Task entry
- Unlock
The external submit (task) full version push method deals with some unusual situations, such as initializing and expanding the work queue array workQueuesworkQueues; creating a new work queue workQueues[i]
createWorker()
After we create the work queue in the externalSubmit(task) method, we need to start the work threads in the work queue and then process the tasks in the work queue. Finally, the externalSubmit(task) method calls the createWorker() method to create the work threads and start the threads.
private boolean createWorker() { ForkJoinWorkerThreadFactory fac = factory; Throwable ex = null; ForkJoinWorkerThread wt = null; try { // Create worker threads and call registerWorker method to bind worker threads to workqueues if (fac != null && (wt = fac.newThread(this)) != null) { // Start engineering threads wt.start(); return true; } } catch (Throwable rex) { ex = rex; } // Finally, unbind the relationship between worker threads and workqueues deregisterWorker(wt, ex); return false; }
runWorker()
ForkJoinWorkerThread.run() eventually calls the ForkJoinPool.runWorker() method to loop through the tasks in the queue.
final void runWorker(WorkQueue w) { // Data for tasks stored in the allocation queue w.growArray(); // allocate queue int seed = w.hint; // initially holds randomization hint int r = (seed == 0) ? 1 : seed; // avoid 0 for xorShift // Spin, perform tasks in the queue for (ForkJoinTask<?> t;;) { // Getting tasks if ((t = scan(w, r)) != null) // Perform tasks w.runTask(t); // Waiting task else if (!awaitWork(w, r)) break; r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift } }
This method eventually calls back to the ForkJoinTask.exec() method, and then to the compute() method of the subclasses RecursiveTask and RecursiveAction for task execution.
ForkJoinTask
fork()
When we call ForkJoinTask's fork method, the program places the task in the queue and executes the task asynchronously. 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 push method stores the current task in the ForkJoinTask array in the work queue, as follows:
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(); } }
join()
The main function of the Join method is to block the current thread and wait for the result. The code is as follows:
public final V join() { int s; if ((s = doJoin() & DONE_MASK) != NORMAL) reportException(s); return getRawResult(); } private void reportException(int s) { if (s == CANCELLED) throw new CancellationException(); if (s == EXCEPTIONAL) rethrow(getThrowableException()); }
First, it calls the doJoin() method to get the status of the current task to determine what results are returned. There are four kinds of task states: completed (NORMAL), cancelled (CANCELLED), signal (SIGNAL) and exceptional (EXCEPTIONAL).
- If the task state is completed, the result of the task is returned directly.
- If the task status is cancelled, the CancellationException is thrown directly.
- If the task state throws an exception, the corresponding exception is thrown directly.
Let's analyze the implementation code of the doJoin() method again.
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). // Perform tasks tryUnpush(this) && (s = doExec()) < 0 ? s : wt.pool.awaitJoin(w, this, 0L) : // Blocking non-worker threads until the worker thread has finished executing 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, firstly, by looking at the status of the task, we can see whether the task has been completed. If the task is completed, we can return to the status of the task directly. If not, we can take the task out of the task array and execute it. If the task is successfully executed, the task state is set to NORMAL. If an exception occurs, the exception is recorded and the task state is set to EXCEPTIONAL.
Reference resources
Art of concurrent programming in java
Source code
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-concurrent project