fork-join 池为 Java 程序员提供了一个高性能、并行、精细的任务执行框架。
它通过分而治之来解决问题。将任务拆分为子任务。任务通过 fork() 方法创建子任务。
当任务客户端提交/调用/执行一个fork join任务时,该任务进入一个共享队列,这个共享队列用来喂非共享双端队列(又名“deque”)由 WorkerThread 管理。
一个或多个 WorkerThreads 称为 Fork-Join 池。
WorkerThread 从 共享队列 中拉出任务,然后他们开始处理工作(使用非共享队列) .
Fork-Join-Pool 中的每个 WorkerThread(实际上是一个 Java 线程)都在一个循环中运行,该循环不断扫描 (sub -)要执行的任务。
我们的目标是尽量让 WorkerThreads 尽可能忙碌,因此我们希望他们总是有事可做。
目标是最大化处理器核心利用率。
每个WorkerThread都有自己的双端队列(又名“deque ") 作为其主要任务来源。
除此之外,其他共享队列曾经将非分叉加入任务放入分叉加入池中,排名第一。
“deque”由 WorkQueue(嵌套在 ForkJoinPool 中的 Java 类)实现。该类中的一些重要方法是 push()、pop() 和 poll()。
在某些时候,任务无法取得任何进展,因为它正在等待通过 join() 方法完成的子任务。
此连接不同于 Java 线程中的连接。
在Java Thread Join中,如果一个任务没有返回结果,就会阻塞,并等待其他线程完成。
如果在 Fork-Join 中 join() 发生阻塞,则 WorkerThread 停止在当前线程上工作并开始执行子任务..
每当您在 RecursiveTask 或 RecursiveAction 内部调用 fork() 内部计算方法时它始终在 fork-join-pool 中的线程上下文中运行。
如果 RecursiveTask 或 RecursiveAction 正在运行的任务由 WorkerThread 调用 fork (),那个新的 ForkJoinTask* 被推到那个工人“deque”线程的头部。
它以 LIFO 顺序推动它,后进先出。
当我们为此任务调用 join() 时,该任务将从“deque”的头部弹出(顶部堆栈)并在 WorkerThread 中运行到完成(继续运行直到完成)。
我们为什么要LIFO?为什么我们在前面推,在前面弹出?为了提高引用的局部性,提高缓存性能,让你尽快得到处理,有时也称为新鲜工作。
ForkJoinTask 支持细粒度的数据并行。
ForkJoinTask 比 Java 线程 更轻量,它没有自己的运行时堆栈。
ForkJoinTask 将数据块与对该数据的计算相关联。
一个真正的 Java 线程有它自己的堆栈、寄存器和许多其他资源,这些资源允许它被操作系统内部的线程调度程序独立管理。
大量的 ForkJoinTask 可以在 Fork-Join-Pool 中少得多的 WorkerThreads 中运行。
WorkerThreads 的数量通常(如果未指定)是内核数量的函数。每个 WorkerThread 都是一个 Java 线程 对象,其中包含您对普通线程的期望。
ForkJoinTask 有两个重要的方法来控制并行处理和合并结果,它们是 fork() 和 join()。
fork() 安排在适当的线程池中异步执行此任务。 fork() 就像 Thread.start() 的轻量版。
fork() 不会创建 Java 工作线程(至少不会直接创建),但最终会在 Java 线程上运行。
它不会立即开始运行,而是将子任务放在工作队列的头部。
A join() 在子任务完成时返回计算。
Fork-Join 池中的加入不同于经典的 Java 线程加入。
Java 线程用作屏障同步器,等待另一个线程完成,然后加入它(直到另一个线程完成后才能继续)。
普通线程中的连接会阻塞调用线程。
Fork-Join 池中的连接不会简单地阻塞调用线程,而是分配 WorkerThread 来运行待处理的子任务。
当 WorkerThread 遇到 join() 时,它会处理任何其他子任务,直到它注意到目标子任务已完成。 WorkerThreads 在此子任务结果完成之前不会返回给调用者。
fork-join 任务中的 Join 不是阻塞的,它持有当前任务,因此只有在 join() 创建的子任务完成后才能继续计算。 p>
WorkerThread 发现,该任务在子任务完成之前被阻塞,因此它开始处理子任务。
WorkerThread 通过从它自己的“deque 中弹出(子)任务,以 LIFO 顺序处理它自己的“deque”强>”。
工作窃取
当一个 WorkerThread 没有其他事情可做时 - “空闲”。如果 WorkerThread 自己的队列是空的,它会去尝试“窃取”一个子- 来自其他繁忙线程“deque”尾部的任务,该线程是随机选择的,以最大限度地提高核心利用率。
任务按 FIFO 顺序“被盗”,因为较旧的被盗任务可能会提供大量工作单元。
Push() 和 pop() 仅由拥有的工作线程调用(到“deque ") 这就是他们最高效的原因,他们使用无等待“Compare-And-Swap” CAS 操作。 CAS 是在内存中原子检查和设置 lock 值的硬件级别 - 它从不阻塞。 push() 和 pop() 有一个非常轻量级的锁定。
Poll() 可以从另一个线程调用以“窃取”作为子任务。当我们调用 poll() 时,这是因为另一个线程已被随机分配以尝试以 FIFO 顺序从该双端队列末尾“窃取”子任务。 Poll() 是由另一个线程发起的,因此它可能并不总是免等待,因此有时它必须“让步”并稍后再试。 “偷”速度很快,但可能不如推拉快。