面试知识库
进阶

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,偷最大的任务来执行)
  → 减少竞争(各取各的端,冲突概率低)
plaintext

ForkJoinTask 使用示例:

parallelStream 与 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