中 进阶
Fork-Join框架#
一句话答案#
Fork/Join 分治并行框架,工作窃取算法让空闲线程从其他线程队列尾部偷取任务,parallelStream 底层基于它。
核心要点
ForkJoinPool 核心思想:分治 + 工作窃取
Fork(分叉):将大任务递归拆分成小任务
Join(合并):将小任务的结果合并成大任务的结果plaintext工作窃取(Work-Stealing)算法:
每个工作线程有自己的双端队列(Deque):
Thread 1: [A1, A2, A3] ← 从头部取自己的任务
Thread 2: [B1, B2] ← 从头部取自己的任务
Thread 3: [空] ← 自己队列空了
Thread 3 的队列空了,不会闲着:
→ 从其他线程(如 Thread 1)的队列尾部"偷"一个任务
→ Thread 3 偷走 A3 来执行
关键设计:
自己的任务从头部取(LIFO,大任务先执行,再细分)
偷别人的任务从尾部取(FIFO,偷最大的任务来执行)
→ 减少竞争(各取各的端,冲突概率低)plaintextForkJoinTask 使用示例:
class SumTask extends RecursiveTask<Long> {
private final long[] array;
private final int start, end;
private static final int THRESHOLD = 10000;
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 小任务直接计算
long sum = 0;
for (int i = start; i < end; i++) sum += array[i];
return sum;
}
// 大任务拆分
int mid = (start + end) / 2;
SumTask left = new SumTask(array, start, mid);
SumTask right = new SumTask(array, mid, end);
left.fork(); // 异步提交左半部分
Long rightResult = right.compute(); // 当前线程计算右半部分
Long leftResult = left.join(); // 等待左半部分结果
return leftResult + rightResult;
}
}
ForkJoinPool pool = new ForkJoinPool(4);
Long result = pool.invoke(new SumTask(array, 0, array.length));javaparallelStream 与 ForkJoinPool 的关系:
parallelStream() 底层使用 ForkJoinPool.commonPool()
commonPool 的线程数 = Runtime.getRuntime().availableProcessors() - 1
问题:整个 JVM 共享一个 commonPool
→ 一个 parallelStream 执行慢(如含 IO 操作)→ 阻塞公共线程
→ 影响其他 parallelStream / CompletableFuture 的执行
解决:自定义 ForkJoinPool 提交任务
ForkJoinPool customPool = new ForkJoinPool(16);
customPool.submit(() ->
list.parallelStream().map(...).collect(...)
).get();plaintext面试回答(2分钟版)
Fork/Join 是 Java 7 引入的并行计算框架,核心思想是分治加工作窃取。Fork 阶段将大任务递归拆分成小任务提交到线程池,小任务执行完后 Join 合并结果。关键设计是工作窃取算法:每个工作线程维护一个双端队列,自己从头部取任务执行,当队列空了就从其他线程的队列尾部偷任务。这样设计减少了线程间的竞争——各取各的端,冲突概率极低,同时保证了负载均衡不会有线程闲着。使用时继承 RecursiveTask(有返回值)或 RecursiveAction(无返回值),在 compute 方法里判断任务是否足够小,小了直接计算,大了就 fork 拆分。parallelStream 底层就是基于 ForkJoinPool.commonPool() 实现的,但 commonPool 是整个 JVM 共享的,一个慢任务会阻塞所有使用公共池的操作,所以生产中有 IO 操作时一定要用自定义的 ForkJoinPool。另外小数据量不要用 parallelStream,线程切换和任务拆分的开销可能比串行执行还大。
追问与易错
追问方向:
- “工作窃取从队列哪头偷?为什么?”→ 从队列尾部偷(FIFO),而线程自己从头部取(LIFO);各取各端减少锁竞争,且尾部通常是较大的未拆分任务,偷过去能干更久
- “parallelStream 的坑?”→ 底层共享 ForkJoinPool.commonPool(),含 IO 操作会阻塞公共线程影响全局;小数据量并行反而更慢(线程切换开销大于计算)
- “什么场景不适合 Fork/Join?”→ 含 IO/阻塞操作的任务、数据量小拆分开销大于收益的场景、任务间有依赖不能独立拆分的场景
易错点:
- ❌ parallelStream 一定比串行快——小数据量反而慢
- ❌ 在 parallelStream 中做 IO——会阻塞 common pool