Running Principle of Fork/Join Framework

Keywords: Programming Spring Java github

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);
}
  1. First, determine whether the work queue array is NULL or not. If you go directly to the following full version of push method
  2. Based on the random number, find the work queue p=workQueues[i] that the current task needs to put in
  3. If p is NULL, go directly to the following full version of push method
  4. Get locks on q queues
  5. Judging whether the queue is full or not, if you go directly to the next full version of the push method
  6. Task entry
  7. 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

Posted by subwayman on Mon, 23 Sep 2019 20:05:52 -0700