让我们深入探讨 Fork/Join 框架的底层工作原理。我会从几个核心机制逐步解释,帮助你理解它是如何实现高效并行处理的。
1. 任务调度与线程池
Fork/Join 框架的核心是 ForkJoinPool 类。它不同于普通的 ThreadPoolExecutor,采用了一种称为"工作窃取"的特殊调度方式。让我们看看它的内部结构:
public class ForkJoinPool extends AbstractExecutorService {
// 工作队列数组,每个线程都有自己的工作队列
volatile WorkQueue[] workQueues;
// 线程的内部类,用于执行任务
static final class ForkJoinWorkerThread extends Thread {
// 每个工作线程都有自己的工作队列
final WorkQueue workQueue;
// 关联的线程池
final ForkJoinPool pool;
}
}
每个工作线程(ForkJoinWorkerThread)都维护着一个双端队列(WorkQueue)。这个队列有两个重要特点:
- 线程自己生成的子任务会从队列头部加入(push)
- 线程自己执行任务时也是从头部取任务(pop)
- 但其他线程窃取任务时是从尾部取任务(poll)
2. 工作窃取算法实现
工作窃取的具体实现是通过 WorkQueue 类完成的。它使用了 Deque(双端队列)的变体来保证并发安全:
final class WorkQueue {
// 存储任务的数组
ForkJoinTask<?>[] array;
// 队列的基本操作
final ForkJoinTask<?> pop() // 从头部取任务
final ForkJoinTask<?> poll() // 从尾部窃取任务
final void push(ForkJoinTask<?> task) // 添加新任务
}
当一个工作线程的队列为空时,它会遵循以下步骤寻找任务:
- 随机选择另一个工作线程
- 尝试从其队列尾部窃取任务
- 如果窃取失败,继续尝试其他线程的队列
- 如果所有队列都为空,线程会进入等待状态
3. 任务分割与执行机制
Fork/Join 框架中的任务(ForkJoinTask)主要有两种实现:RecursiveTask(有返回值)和 RecursiveAction(无返回值)。让我们看看它们的执行机制:
public abstract class ForkJoinTask<V> implements Future<V> {
// 任务的执行状态
volatile int status;
// 核心方法
public final ForkJoinTask<V> fork() {
Thread t = Thread.currentThread();
if (t instanceof ForkJoinWorkerThread) {
((ForkJoinWorkerThread)t).workQueue.push(this);
} else {
// 如果不是在 ForkJoinPool 中调用,会创建新的 ForkJoinPool
ForkJoinPool.common.execute(this);
}
return this;
}
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
}
4. 任务同步与结果合并
join() 操作的实现使用了一种叫做"帮助完成"(helping)的机制:
- 当线程调用 join() 等待某个任务的结果时,它不会简单地阻塞
- 相反,它会先检查是否有其他任务可以执行
- 如果有,它会先帮助执行其他任务,这样可以提高整体效率
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0L) :
externalAwaitDone();
}
5. 异常处理机制
Fork/Join 框架对异常处理也做了特殊设计:
public final void quietlyComplete() {
setCompletion(NORMAL);
}
void setExceptionalCompletion(Throwable ex) {
int s = status & ~DONE_MASK;
// 记录异常状态
status = s | EXCEPTIONAL;
setException(ex);
}
当子任务发生异常时:
- 异常会被捕获并存储在任务对象中
- 父任务在调用 join() 时可以获取到这个异常
- 这样可以保证一个子任务的异常不会影响其他子任务的执行
了解了这些底层机制,我们就能更好地理解为什么 Fork/Join 框架能够高效处理并行任务:
- 工作窃取算法确保了负载均衡
- 双端队列的设计减少了线程竞争
- "帮助完成"机制提高了线程利用率
- 异常处理机制保证了任务的可靠执行
欢迎评论区留言讨论