【线程池ForkJoinPool原理】

一、由一道算法题引发的思考

算法题:如何充分利用多核CPU的性能,快速对一个2千万大小的数组进行排序?
分解 求解 合并
这道算法题可以拆解来看:
1)首先这是一道排序的算法题,而且是需要使用高效的排序算法对2千万大小的数组进行排序,可以
考虑使用快速排序或者归并排序。
2)可以使用多线程并行排序算法来充分利用多核CPU的性能。

2. 基于归并排序算法实现

对于大小为2千万的数组进行快速排序,可以使用高效的归并排序算法来实现。

2.1 什么是归并排序

归并排序(Merge Sort)是一种基于分治思想的排序算法。归并排序的基本思想是将一个大数组分成
两个相等大小的子数组,对每个子数组分别进行排序,然后将两个子数组合并成一个有序的大数组。
因为常常使用递归实现(由先拆分后合并的性质决定的),所以我们称其为归并排序。
归并排序的步骤包括以下几个方面:
将数组分成两个子数组
对每个子数组进行排序
合并两个有序的子数组
归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。
分治思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题
性质相同。求出子问题的解,就可得到原问题的解。
分治思想的步骤如下:

  1. 分解:将要解决的问题划分成若干规模较小的同类问题;
  2. 求解:当子问题划分得足够小时,用较简单的方法解决;
  3. 合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。
    计算机十大经典算法中的归并排序、快速排序、二分查找都是基于分治思想实现的算法

2.2 使用归并排序实现上面的算法题

单线程实现归并排序
单线程归并算法的实现,它的基本思路是将序列分成两个部分,分别进行递归排序,然后将排序好的子序列合并起来。

2.3 Fork/Join并行归并排序

并行归并排序是一种利用多线程实现的归并排序算法。它的基本思路是将数据分成若干部分,然后在
不同线程上对这些部分进行归并排序,最后将排好序的部分合并成有序数组。在多核CPU上,这种算
法也能够有效提高排序速度。
可以使用Java的Fork/Join框架来实现归并排序的并行化

2.4 并行实现归并排序的优化和注意事项

在实际应用中,我们需要考虑数据分布的均匀性、内存使用情况、线程切换开销等因素,以充分利用
多核CPU并保证算法的正确性和效率。
以下是并行实现归并排序的一些优化和注意事项:
任务的大小:任务大小的选择会影响并行算法的效率和负载均衡,如果任务太小,会造成任务划分和合并的开销
过大;如果任务太大,会导致任务无法充分利用多核CPU并行处理能力。因此,在实际应用中需要根据数据量、
CPU核心数等因素选择合适的任务大小。
负载均衡:并行算法需要保证负载均衡,即各个线程执行的任务大小和时间应该尽可能相等,否则会导致某些线
程负载过重,而其他线程负载过轻的情况。在归并排序中,可以通过递归调用实现负载均衡,但是需要注意递归
的层数不能太深,否则会导致任务划分和合并的开销过大。
数据分布:数据分布的均匀性也会影响并行算法的效率和负载均衡。在归并排序中,如果数据分布不均匀,会导
致某些线程处理的数据量过大,而其他线程处理的数据量过小的情况。因此,在实际应用中需要考虑数据的分布
情况,尽可能将数据分成大小相等的子数组。
内存使用:并行算法需要考虑内存的使用情况,特别是在处理大规模数据时,内存的使用情况会对算法的执行效
率产生重要影响。在归并排序中,可以通过对数据进行原地归并实现内存的节约,但是需要注意归并的实现方
式,以避免数据的覆盖和不稳定排序等问题。
线程切换:线程切换是并行算法的一个重要开销,需要尽量减少线程的切换次数,以提高算法的执行效率。在归
并排序中,可以通过设置线程池的大小和调整任务大小等方式控制线程的数量和切换开销,以实现算法的最优性能。

3. Fork/Join框架介绍

3.1 什么是Fork/Join

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

3.2 应用场景

Fork/Join框架的应用场景包括以下几个方面:

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

3.3 Fork/Join使用

Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于
管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。
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时则使
用后进先出的模式。

和普通线程池之间的区别

  • 工作窃取算法

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

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

  • 工作线程的数量

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

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

3.4 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是用于外
部线程提交任务的,而且是由多个线程共享的,因此它们不能被线程窃取任务。

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是的,ForkJoinPoolJava中的一个线程池,主要用于执行分治任务。它是Java 7引入的一个新特性,可以利用多核处理器提高并行计算性能。ForkJoinPool使用工作窃取算法,即当一个线程的任务执行完后,它会从其他线程的任务队列中窃取任务来执行,以保证各个线程的任务负载较为均衡。 ForkJoinPool的使用方法与其他线程池类似,可以通过构造函数或者静态工厂方法来创建线程池。例如: ``` ForkJoinPool pool = new ForkJoinPool(); ``` 这样就创建了一个默认的ForkJoinPool线程池,它的线程数等于CPU核心数。也可以通过构造函数来指定线程池的参数,例如: ``` ForkJoinPool pool = new ForkJoinPool(4); ``` 这样就创建了一个包含4个线程的ForkJoinPool线程池。在使用ForkJoinPool时,需要定义一个ForkJoinTask任务,例如: ``` class MyTask extends RecursiveTask<Integer> { protected Integer compute() { // 执行任务 } } // 创建任务 MyTask task = new MyTask(); // 执行任务 int result = pool.invoke(task); ``` 这里的MyTask是一个继承自ForkJoinTask的任务,它的compute()方法中定义了任务的具体执行过程。执行任务的方式是通过ForkJoinPool的invoke()方法来调用,它会返回任务的执行结果。 当然,除了invoke()方法之外,ForkJoinPool还提供了其他一些方法来执行任务,例如submit()和execute()方法。同时,ForkJoinPool也支持设置线程池的一些属性,例如任务窃取的策略、线程池的名称等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值