Fork/Join框架介绍

目录

1. Fork/Join框架

2.  Fork/Join应用场景

3.  Fork/Join使用

3.1 ForkJoinPool

3.1.1 ForkJoinPool基础知识

3.1.2 ForkJoinPool工作原理  

3.1.3 ForkJoinPool执行流程

3.2 ForkJoinTask

4 小结


1. Fork/Join框架

        Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。

2.  Fork/Join应用场景

Fork/Join框架的应用场景包括以下几个方面:
1. 递归分解型任务
        Fork/Join框架特别适用于递归分解型的任务,例如排序、归并、遍历等。这些任务通常可以将大的任务分解成若干个子任务,每个子任务可以独立执行,并且可以通过归并操作将子任务的结果合并成一个有序的结果。
2. 数组处理
        Fork/Join框架还可以用于数组的处理,例如数组的排序、查找、统计等。在处理大型数组时,Fork/Join框架可以将数组分成若干个子数组,并行地处理每个子数组,最后将处理后的子数组合并成一个有序的大数组。
3. 并行化算法
        Fork/Join框架还可以用于并行化算法的实现,例如并行化的图像处理算法、并行化的机器学习算法等。在这些算法中,可以将问题分解成若干个子问题,并行地解决每个子问题,然后将子问题的结果合并起来得到最终的解决方案。
4. 大数据处理
        Fork/Join框架还可以用于大数据处理,例如大型日志文件的处理、大型数据库的查询等。在处理大数据时,可以将数据分成若干个分片,并行地处理每个分片,最后将处理后的分片合并成一个完整的结果。

3.  Fork/Join使用

        Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。

3.1 ForkJoinPool

3.1.1 ForkJoinPool基础知识

        ForkJoinPool是Fork/Join框架中的线程池类,它用于管理Fork/Join任务的线程。ForkJoinPool类包括一些重要的方法,例如submit()、invoke()、shutdown()、awaitTermination()等,用于提交任务、执行任务、关闭线程池和等待任务的执行结果。ForkJoinPool类中还包括一些参数,例如线程池的大小、工作线程的优先级、任务队列的容量等,可以根据具体的应用场景进行设置。

构造器:
ForkJoinPool中有四个核心参数,用于控制线程池的 并行数、工作线程的创建、异常处理和模式指定 等。各参数解释如下:
  • int parallelism指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如 果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
  • boolean asyncMode:设置队列的工作模式。当asyncMode为true时,将使用先进先出队列,而为false时则使 用后进先出的模式
//获取处理器数量 1
int processors = Runtime.getRuntime().availableProcessors(); 2
//构建forkjoin线程池 3
ForkJoinPool forkJoinPool = new ForkJoinPool(processors);

任务提交方式  

任务提交是ForkJoinPool的核心能力之一,提交任务有三种方式:
1. 提交异步执行
2. 等待并获取结果
3.  提交执行获取Future结果

和普通线程池之间的区别

工作窃取算法 :ForkJoinPool采用工作窃取算法来提高线程的利用率,而普通线程池则采用任务队列来管理任务。在 工作窃取算法中,当一个线程完成自己的任务后,它可以从其它线程的队列中获取一个任务来执行, 以此来提高线程的利用率。

任务的分解和合并 ForkJoinPool可以将一个大任务分解为多个小任务,并行地执行这些小任务,最终将它们的结果合并 起来得到最终结果。而普通线程池只能按照提交的任务顺序一个一个地执行任务。

工作线程的数量:

ForkJoinPool会根据当前系统的CPU核心数来自动设置工作线程的数量,以最大限度地发挥CPU的性 能优势。而普通线程池需要手动设置线程池的大小,如果设置不合理,可能会导致线程过多或过少, 从而影响程序的性能。

 任务类型 ForkJoinPool适用于执行大规模任务并行化,而普通线程池适用于执行一些短小的任务,如处理请求 等。

3.1.2 ForkJoinPool工作原理  

        ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提 交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中 会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。
        如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫 做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务。如此 一来,所有的工作线程都不会闲下来了。
工作线程ForkJoinWorkerThread
        ForkJoinWorkerThread是ForkJoinPool中的一个专门用于执行任务的线程。 当一个ForkJoinWorkerThread被创建时,它会自动注册一个WorkQueue到ForkJoinPool中。这个 WorkQueue是该线程专门用于存储自己的任务的队列,只能出现在WorkQueues[]的奇数位。在 ForkJoinPool中,WorkQueues[]是一个数组,用于存储所有线程的WorkQueue。
工作队列WorkQueue
WorkQueue是一个双端队列,用于存储工作线程自己的任务。每个工作线程都会维护一个本地的
WorkQueue,并且优先执行本地队列中的任务。当本地队列中的任务执行完毕后,工作线程会尝试从
其他线程的WorkQueue中窃取任务。
注意:在ForkJoinPool中,只有WorkQueues[]奇数位的WorkQueue是属于ForkJoinWorkerThread
线程的,因此只有这些WorkQueue才能被线程本身使用和窃取任务。偶数位的WorkQueue是用于外
部线程提交任务的,而且是由多个线程共享的,因此它们不能被线程窃取任务。
工作窃取
        ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于, ForkJoinPool存在引入了工作窃 取设计 ,它是其性能保证的关键之一。 工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任 务。 默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程 会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能 性
        ForkJoinPool的大部分操作都发生在工作窃取队列(work-stealing queues ) 中,该队列由内部 类WorkQueue实现。 它是Deques的特殊形式,但仅支持三种操作方式:push、pop和poll(也称为 窃取) 。在ForkJoinPool中,队列的读取有着严格的约束,push和pop仅能从其所属线程调用,而 poll则可以从其他线程调用。 通过工作窃取,Fork/Join框架可以实现任务的自动负载均衡,以 充分利用多核CPU的计算能力,同时 也可以避免线程的饥饿和延迟问题

3.1.3 ForkJoinPool执行流程

 

3.2 ForkJoinTask

        ForkJoinTask是Fork/Join框架中的抽象类,它定义了执行任务的基本接口。用户可以通过继承 ForkJoinTask类来实现自己的任务类,并重写其中的compute()方法来定义任务的执行逻辑。通常情 况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子 类:
RecursiveAction 用于 递归执行但不需要返回结果的任务。
RecursiveTask 用于 递归执行需要返回结果的任务。
CountedCompleter<T> :在任务完成执行后会触发执行一个自定义的钩子函数
调用方法
        ForkJoinTask 最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提 交,一个用于结果获取。
fork()——提交任务 :fork()方法 用于向当前任务所运行的线程池中提交任务。如果当前线程ForkJoinWorkerThread 类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
join()——获取任务执行结果 :join()方法用于 获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果
 
处理递归任务注意事项
对于一些递归深度较大的任务,使用Fork/Join框架可能会出现任务调度和内存消耗的问题。
当递归深度较大时,会产生大量的子任务,这些子任务可能被调度到不同的线程中执行,而线程的创
建和销毁以及任务调度的开销都会占用大量的资源,从而导致性能下降。
此外,对于递归深度较大的任务,由于每个子任务所占用的栈空间较大,可能会导致内存消耗过大,
从而引起内存溢出的问题。
因此, 在使用Fork/Join框架处理递归任务时,需要根据实际情况来评估递归深度和任务粒度 ,以避免
任务调度和内存消耗的问题。如果递归深度较大,可以尝试采用其他方法来优化算法,如使用迭代方
式替代递归,或者限制递归深度来减少任务数量,以避免Fork/Join框架的缺点。

处理阻塞任务

  1. 防止线程饥饿:当一个线程在执行一个阻塞型任务时,它将会一直等待任务完成,这时如果没有其他线程可以窃 取任务,那么该线程将一直被阻塞,直到任务完成为止。为了避免这种情况,应该避免在ForkJoinPool中提交大 量的阻塞型任务。
  2. 使用特定的线程池:为了最大程度地利用ForkJoinPool的性能,可以使用专门的线程池来处理阻塞型任务,这些 线程不会被ForkJoinPool的窃取机制所影响。例如,可以使用ThreadPoolExecutor来创建一个线程池,然后将 这个线程池作为ForkJoinPool的执行器,这样就可以使用ThreadPoolExecutor来处理阻塞型任务,而使用 ForkJoinPool来处理非阻塞型任务。
  1. 不要阻塞工作线程:如果在ForkJoinPool中使用阻塞型任务,那么需要确保这些任务不会阻塞工作线程,否则会 导致整个线程池的性能下降。为了避免这种情况,可以将阻塞型任务提交到一个专门的线程池中,或者使用 CompletableFuture等异步编程工具来处理阻塞型任务

4 小结

        Fork/Join是一种基于分治思想的模型,在并发处理计算型任务时有着显著的优势。其效率的提升主要 得益于两个方面:
  1. 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
  2. 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。
        在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关 心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方 案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑜伽娃娃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值