在深入聊 ForkJoinPool 前,我们先聊聊 ForkJoinPool 与 ThreadPoolExecutor的区别。
我们为啥要用 ForkJoinPool ?
相比于我们更常用的 ThreadPoolExecutor ,ForkJoinPool 又能给我们带来什么呢?
带着这样的问题我们来好好聊聊。
1.首先他们都继承自 AbstractExecutorService
但 ForkJoinPool 并不是为了替代 ThreadPoolExecutor 而产生的,相对来说 ForkJoinPool 是对线程池使用场景和功能上进行了一个补充
public class ForkJoinPool extends AbstractExecutorService
public class ThreadPoolExecutor extends AbstractExecutorService
2.构造函数不同
ThreadPoolExecutor 不是本篇重点,构造函数就不细讲了,相信大家也比较熟悉了。
我们重点说下 ForkJoinPool
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
int mode,
String workerNamePrefix)
"ForkJoinPool-" + nextPoolId() + "-worker-"
3.工作模式不同
ForkJoinPool 采用了 一个线程对应专属的一个工作队列,而非 ThreadPoolExecutor 的多个线程对应一个工作队列。即 线程与工作队列关系 由 多对一 变为 一对一
ThreadPoolExecutor 线程池模型如下
ForkJoinPool 的线程与工作队列对应模型
其实对于 ForkJoinPool 的整个工作流程,和 ThreadPoolExecutor 还是有很大的区别的,在这里我围绕 Fork/Join 仅阐明比较核心的几个概念:
工作窃取
对于 ForkJoinPool 来说,任务提交有两种:
一种是直接通过 ForkJoinPool 来提交的外部任务 external/submissions task
第二种是内部 fork 分割的子任务 Worker task
也就是下面这两种方法
forkJoinPool.submit(forkJoinTask);
forkJoinTask.fork();
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task;
}
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;
}
很清晰的给出了两种入队方式
// 内部直接入队,当前线程绑定的队列
((ForkJoinWorkerThread)t).workQueue.push(this);
// 外部入队
externalPush(task);
// 实现
final void externalPush(ForkJoinTask<?> task) {
WorkQueue[] ws; WorkQueue q; int m;
int r = ThreadLocalRandom.getProbe();
int rs = runState;
if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
(q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
U.compareAndSwapInt(q, QLOCK, 0, 1)) {
ForkJoinTask<?>[] a; int am, n, s;
if ((a = q.array) != null &&
(am = a.length - 1) > (n = (s = q.top) - q.base)) {
int j = ((am & s) << ASHIFT) + ABASE;
U.putOrderedObject(a, j, task);
U.putOrderedInt(q, QTOP, s + 1);
U.putIntVolatile(q, QLOCK, 0);
if (n <= 1)
signalWork(ws, q);
return;
}
U.compareAndSwapInt(q, QLOCK, 1, 0);
}
externalSubmit(task);
}
这里提一下 ForkJoinPool 会维护一个 workQueues 也就是所有的工作队列的数组,这个意图有些类似于 HashMap 的 底层数组。
WorkQueue q;
int r = ThreadLocalRandom.getProbe();
m = (ws.length - 1)
q = ws[m & r & SQMASK]
所以外部入队是进入到 ws[m & r & SQMASK]
WorkQueue 的 m & r & SQMASK
位置的工作队列中
对于 int r = ThreadLocalRandom.getProbe(); 做一个简单的解释这里不做深究
使用 ThreadLocalRandom.getProbe() 得到线程的探针哈希值。
在这里,这个探针哈希值的作用是哈希线程,将线程和数组中的不用元素对应起来,尽量避免线程争用同一数组元素。探针哈希值和 map 里使用的哈希值的区别是,当线程发生数组元素争用后,可以改变线程的探针哈希值,让线程去使用另一个数组元素,而 map 中 key 对象的哈希值,由于有定位 value 的需求,所以它是一定不能变的。
回到正题,我们来聊聊 工作窃取(work-stealing)
该算法是指某个线程从其他队列里窃取任务来执行。
ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,每次从队尾取出任务来执行。
每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务会从队首窃取,也就是说窃取任务和执行自己的队列任务的方式是分开的,这样可以尽可能的避免竞争。 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
ForkJoinTask fork / join
想要真正标准的使用 fork/join 框架,那么 ForkJoinTask 是必不可少的。
public abstract class ForkJoinTask<V> implements Future<V>, Serializable
作为一个抽象类,我们需要对其进行实现,但是通常来说我们会继承其子类来进行实现。fork/join 框架为我们提供了三类实现:
我们可以根据自己的业务场景,选择合适的 Task 以及定义其实现。
fork & join
fork 做的事情是将当前的任务,推入当前工作线程的工作队列中
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;
}
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
join 的整体流程如下
其实聊到这里,ForkJoinPool 以及 ForkJoinTask 的核心内容基本都已经介绍差不多了,而在实际的使用中,我们的一般步骤为: