ForkJoinPool用法及原理

1.什么是ForkJoinPool

ForkJoinPool可以理解为能够将提交的任务进行拆分的线程池,简单来说如果我们提交一个从1累加到10亿的值,如果这个任务提交给线程池,线程池中不论有多少个线程都只会拿出其中一条线程执行这个任务,但是如果这个任务提交给ForkJoinPool他能够将这个大任务进行拆分执行,能够利用上我们我们定义ForkJoinPool中的所有线程

2.ForkJoinPool和ThreadPool的区别

ForkJoinPool 主要用于实现“分治法”(Divide and Conquer)策略的任务,这些任务可以被分解成更小的子任务,直到子任务足够简单可以直接处理。它特别适用于那些能够递归地将问题分解为更小的问题的情况,在实际业务中如果执行的业务结束了我们会调用forkJoinPool.sudown()方法用于结束分支合并池,下次使用时再重新创建该池

ThreadPool主要是用于管理系统线程,避免线程的频繁创建和销毁导致的不必要的损耗,一般在系统中运行过程不会使用sudown()命令来结束线程池,只是在线程完成提交的任务时会重新回到池内进行等待下一次任务,并且自定义线程池可以定义核心线程数量和最大线程数量线程死亡时间和队列类型还有拒绝策略,这些都是ForkJoinPool没有的。

3ForkJoinPool使用方法

(1.1)创建适用ForkJoinPool的任务(有返回值)

        我们将定义一个累加的工作

package forkJoin;

import java.util.concurrent.RecursiveTask;

public class SumTask extends RecursiveTask {

    private final int begin; //起始值1
    private final int end;   //结束值20万
    private final int threshold = 10000; //临界值 既 希望拆分的最小单位为1万

    public SumTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Long compute() {
        //总数
        long sum = 0;
        // 判断是否需要拆分 (如果小于1万则不需要拆分)
        if ((end - begin + 1) <= threshold) { // 更精确的条件判断
            for (int i = begin; i <= end; i++) {
                sum += i;
            }
        } else {
            //  创建两个子任务
            int middle = (begin + end) / 2;
            SumTask task1 = new SumTask(begin, middle);
            SumTask task2 = new SumTask(middle + 1, end);
            //  执行子任务
            task1.fork();
            task2.fork();
            //等待子任务结束
            Long join1 = task1.join();
            Long join2 = task2.join();
            // 合并结果
            sum = join1 + join2;
        }

        return sum; // 返回计算结果
    }

}

(1.2)创建分支合并池进行任务提交和执行

 public static void main(String[] args) {


        ForkJoinPool forkJoinPool=new ForkJoinPool(5);
        try {
            System.out.println(forkJoinPool.submit(new SumTask(1, 2000000)).get());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            forkJoinPool.shutdown(); //执行完毕关闭ForkJoinPool
        }


    }

4ForkJoinPool的优势和应注意的事项

4.1工作窃取算法

ForkJoinPool 实现了工作窃取(Work-Stealing)算法。每个线程都有自己的任务队列,当一个线程完成了自己队列中的任务后,它可以“窃取”其他线程队列中的任务来执行。这不仅提高了资源利用率,也使得任务分配更加均衡,减少了线程的空闲时间。


4.2高效的并行处理

特别适合于能够递归分解的大规模任务。通过将任务分割成更小的子任务,并行地处理这些子任务,然后合并结果,这种方式在处理大量数据时效率非常高,例如排序、搜索等操作。


4.3减少上下文切换

与传统线程池相比,由于任务被有效地分配给可用的线程,减少了创建和销毁线程的开销以及上下文切换的频率,从而提高了性能。

4.4注意事项

传统业务中大多数情况都是IO密集型业务,使用ForkJoinPool的场景往往是CPU密集型任务

例如:

快速排序
并行归并
图像处理
大数组的求和/查找等

5原理

核心概念

  1. 双端队列(Deque):每个线程都有自己的双端队列来存储任务。在 ForkJoinPool 中,任务通常是从队列的一端被取出(通常是尾部),而当一个线程需要“窃取”工作时,它会从另一个线程队列的另一端(通常是头部)获取任务。

  2. 任务分配:当一个新的任务被提交到 ForkJoinPool 中时,它会被分配给一个可用的工作线程,并加入到该线程的双端队列中。如果一个线程完成了自己队列中的所有任务并且发现池中还有其他未完成的任务,则可以尝试从其他线程的队列中窃取任务。

  3. 减少竞争:通过让每个线程主要从自己的队列中取任务,以及只在必要时从其他线程的队列中“窃取”,这种方法减少了不同线程之间的直接竞争,从而降低了同步开销。

工作流程

  • 任务生成与分割:当一个任务被提交给 ForkJoinPool 时,如果这个任务足够大,它可以进一步分解为更小的子任务。

  • 本地执行:子任务通常首先尝试在创建它们的同一个线程上执行(即在当前线程的双端队列中添加并处理这些任务)。

  • 任务窃取:一旦某个线程完成了其队列中的所有任务,它将开始搜索其他线程的队列以寻找可执行的任务。为了最小化竞争,窃取者总是试图从目标队列的另一端(不同于添加任务的那一端)移除任务。

你可能感兴趣的:(ForkJoinPool用法及原理)