当前位置: 首页 > 后端技术 > Java

5分钟真正看懂ForkJoin是如何“分而治之”,竟然有这些隐藏的坑

时间:2023-04-01 21:33:41 Java

5分钟真正理解ForkJoin是如何“分而治之”的,还有这些隐藏的坑利用CPU资源将大任务拆分成很多小子任务,多线程并行。但是你知道吗:拆分子任务的计算量是否合理?为什么使用ForkJoin却降低了性能?大量并行线程,如何避免线程阻塞?虽然我们知道“先fork后join”,但是谁负责join呢?有没有需要注意的陷阱?ForkJoinPool的invoke和submit启动方法有隐藏陷阱你知道吗?本期眼见为实系列,我们将通过Kindling程序摄像头查看一次请求计算下所有ForkJoin线程的工作记录和分析,来探究上述问题的答案。实验一:分析ForkJoin线程工作首先,我写了一个基于ForkJoin的实验demo:计算1-7的和。下图是程序摄像头抓拍到的一个请求计算的trace分析。不熟悉kindling程序相机操作的同学可以参考我们的操作手册:http://www.kindling.space:332...页面概览螺纹轴缩放(上图中futex表示螺纹是等待,我让线程执行计算他们都休眠了一段时间,方便放大线程轴观察)通过各个线程的log,我们发现ForkJoinPool-worker-1线程最先拿到了fork1~7个任务,拆解成子任务1(计算1~4),子任务2(计算5~7),因为我设置的ForkJoin的最小任务计算数是3,所以子任务1还是需要fork,子任务2可以直接计算。最终的拆解结果如下图所示:我们可以看到,worker-2线程拆解出来的子任务1~2被丢给了worker-1线程进行计算。另外,worker-2最后加入了子任务1到4的结果,worker-1加入了1到7的结果,也就是谁fork了task,谁就加入task。通过这个实验我们也可以看出,当一个线程执行的时间比较长时,需要加入其结果的线程会一直等待下去。如下图红框所示,我让worker-2在加入results1到4后休眠了30ms,然后worker-1的线程在这段时间被锁住了。点击黄色锁块,我们可以看到栈信息,很明显,是在等待worker-2sleep结束。这也是为什么ForkJoin适合CPU密集型计算而不适合IO密集型计算的原因,因为磁盘IO和网络IO的运行特性都是等待,很容易造成线程阻塞。另外,我们的子任务的拆分要合理,避免任务阻塞。那么如何拆分子任务才合理呢?我的实验demo的拆解其实是不合理的。需要考虑创建子任务、线程调度等操作都会消耗时间和内存。官网文档给出的经验是:最小的子任务需要计算100到10000个基本计算步骤,但是这个相对最优的方案应该结合自己的实际业务需求在实践中得到。您可以使用Kindling程序相机来完成它。设置不同的子任务粒度后,可以观察计算的响应时间,以及各个线程的工作状态和耗时分析。实验二:ForkJoinPool的submit和invoke启动方法invoke(ForkJoinTask),submit(ForkJoinTask)都有返回值,invoke的任务会同步到主线程,而submit是异步执行的,需要同步到主线程thread通过task.get,什么意思?我们做了一个实验,下面是实验demo:请求开始记录日志,然后分别使用invoke和submit开始ForkJoinPool测试,请求结束也记录日志,然后sleep500ms(把线轴放大,等以后再说吧)大家看得更清楚)。下图是两种情况下的测试结果。submit启动invoke我们可以看到submit启动线程池时,主线程和ForkJoin线程是异步执行的,而启动invoke时,主线程是等待ForkJoin线程同步执行的。等待的时候,主线程被锁定(也就是上图中黄色高亮的4个问号块),点击查看堆栈确认确实在等待。通过这个实验,我们发现在使用forkJoin的时候一定要注意pool的启动方式,否则会有隐藏的坑,比如下面这个场景:先用forkJoin计算所有订单的利润,保存在数据库order中表,然后根据订单利润计算出每次销售的订单佣金,存入佣金表。如果开发者不注意,在计算佣金的时候,取了订单表的利润,然后使用submitstart方法,可能会导致计算佣金的时候没有计算利润。以上就是本次关于ForkJoin的分享内容。你想通过Kindling相机观察到哪些场景知识?微服务RPC调用机制?爪哇锁?Java分布式?线程安全?线程池调优?Redis集群机制?欢迎加微信好友告诉小编~