多线程中的分治思想-ForkJoin

1,128 阅读8分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

ForkJoin 模式先把一个大任务分解(Fork)成许多个独立的子任务,然后起多线程并行去处理这些子任务。有可能子任务还是很大, 还需要进一步拆解, 最终得到足够小的任务。 image.png

ForkJoin 模式借助了现代计算机多核的优势并行去处理数据。

通常情况下,ForkJoin 模式将分解Fork出来的子任务放入双端队列中, 然后几个启动线程从双端队列中获取任务并执行。子任务执行的结果放到一个队列里, 另起线程从队列中获取数据, 然后再进行局部结果的合并Join 得到了最终结果。

ForkJoin 框架

JUC 包提供了一套 ForkJoin 框架的实现,具体以 ForkJoinPool 线程池的形式提供,并且该线程池在 Java 8 的 Lambda 并行流实现中充当着底层框架的角色。

JUC 包的 ForkJoin 框架包含了如下组件:

  1. ForkJoinPool: 执行任务的线程池,继承了AbstractExecutorService类。

  2. ForkJoinWorkerThread: 执行任务的工作线程(即 ForkJoinPool 线程池里的线程)。 每个线程都维护着一个内部队列,用于存放“内部任务”。继承了 Thread 类。

  3. ForkJoinTask: 一个用于 ForkJoinPool 的任务抽象类,实现了 Future 接口。

    3.1 RecursiveTask: 带返回结果的递归执行任务,是 ForkJoinTask 的子类,在子任务带返回结果时使用。

    3.2 RecursiveAction: 不返回结果的递归执行任务,是 ForkJoinTask 的子类,在子任务不 带返回结果时使用。


日常使用时,我们不会直接实现ForkJoinTask接口,而是直接按照自己的需求使用带返回值的 RecursiveTask, 或者不带返回值的 RecursiveAction 接口。实现compute()方法,方法体中具体对任务进行执行或者拆分,拆分的过程很像很像归并排序的套路。一般的伪代码如下:

if 
    任务足够小 直接返回结果
else
    分割成 N 个子任务
    依次调用每个子任务的 fork 方法执行子任务
    依次调用每个子任务的 join 方法,等待子任务的完成,然后合并执行结果

案例实战

实现从0累加到1000000

定义任务实现

  1. 提前定义好了任务的规模,即,THRESHOLD = 100000如果累加的区间小于该阈值,那么直接进行计算,否则要对任务进行一分为二进行拆分。
public class AccumulateTask extends RecursiveTask<Long> {

    private final long start;

    private final long end;

    private static final long THRESHOLD = 100000;

    public AccumulateTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long sum = 0;
        if (end - start <= THRESHOLD) {
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.println("task-"+Thread.currentThread().getName()+"-执行任务,计算" + start + "到" + end + "的和,结果是:" + sum);
        } else {
            // 任务过大,需要切割,Recursive 递归计算
            System.out.println("task-"+Thread.currentThread().getName()+"-切割任务:将" + start + "到" + end + "的和一分为二");
            long middle = (start + end) / 2;
            // 切割成两个子任务
            AccumulateTask lTask = new AccumulateTask(start, middle);
            AccumulateTask rTask = new AccumulateTask(middle + 1, end);
            // 依次调用每个子任务的 fork 方法执行子任务
            lTask.fork();
            rTask.fork();
            // 等待子任务的完成,依次调用每个子任务的 join 方法合并执行结果
            long leftResult = lTask.join();
            long rightResult = rTask.join();
            // 合并子任务执行结果
            sum = leftResult + rightResult;
        }
        return sum;
    }
}
  1. 任务提交
@Test
public void test() throws ExecutionException, InterruptedException {
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    ForkJoinTask<Long> task = forkJoinPool.submit(new AccumulateTask(0, 1000000));
    System.out.println(task.get());
}
  1. 过程&结果输出
task-ForkJoinPool-1-worker-1-切割任务:将01000000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将0500000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将5000011000000的和一分为二
task-ForkJoinPool-1-worker-1-切割任务:将0250000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将250001500000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将500001750000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将250001375000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将125001250000的和一分为二
task-ForkJoinPool-1-worker-1-切割任务:将0125000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将500001625000的和一分为二
task-ForkJoinPool-1-worker-0-执行任务,计算250001312500的和,结果是:17578156250
task-ForkJoinPool-1-worker-3-执行任务,计算500001562500的和,结果是:33203156250
task-ForkJoinPool-1-worker-2-执行任务,计算125001187500的和,结果是:9765656250
task-ForkJoinPool-1-worker-1-执行任务,计算062500的和,结果是:1953156250
task-ForkJoinPool-1-worker-0-执行任务,计算312501375000的和,结果是:21484406250
task-ForkJoinPool-1-worker-1-执行任务,计算62501125000的和,结果是:5859406250
task-ForkJoinPool-1-worker-2-执行任务,计算187501250000的和,结果是:13671906250
task-ForkJoinPool-1-worker-0-切割任务:将375001500000的和一分为二
task-ForkJoinPool-1-worker-3-执行任务,计算562501625000的和,结果是:37109406250
task-ForkJoinPool-1-worker-3-切割任务:将625001750000的和一分为二
task-ForkJoinPool-1-worker-2-执行任务,计算437501500000的和,结果是:29296906250
task-ForkJoinPool-1-worker-0-执行任务,计算375001437500的和,结果是:25390656250
task-ForkJoinPool-1-worker-3-执行任务,计算625001687500的和,结果是:41015656250
task-ForkJoinPool-1-worker-0-切割任务:将7500011000000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将750001875000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将8750011000000的和一分为二
task-ForkJoinPool-1-worker-3-执行任务,计算687501750000的和,结果是:44921906250
task-ForkJoinPool-1-worker-2-执行任务,计算875001937500的和,结果是:56640656250
task-ForkJoinPool-1-worker-2-执行任务,计算9375011000000的和,结果是:60546906250
task-ForkJoinPool-1-worker-0-执行任务,计算750001812500的和,结果是:48828156250
task-ForkJoinPool-1-worker-3-执行任务,计算812501875000的和,结果是:52734406250
500000500000

ForkJoin核心构造参数

public ForkJoinPool(int parallelism,
                    ForkJoinWorkerThreadFactory factory,
                    UncaughtExceptionHandler handler,
                    boolean asyncMode) {
    this(checkParallelism(parallelism),
         checkFactory(factory),
         handler,
         asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
         "ForkJoinPool-" + nextPoolId() + "-worker-");
    checkPermission();
}
  1. parallelism: 可并行级别

    ForkJoin 框架将依据 parallelism 设定的级别,决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程进行处理,但 parallelism 属性并不是 ForkJoin 框架中最大的线程数量;该属性也和 ThreadPoolExecutor 线程池中的 corePoolSizemaximumPoolSize 属性有区别,因为 ForkJoinPool的结构和工作方式与ThreadPoolExecutor完全不一样。ForkJoin框架中可存在的线程数量和 parallelism 参数值并不是绝对的关联.

  2. factory: 线程创建工厂

    2.1 当 ForkJoin 框架创建一个新的线程时,同样会用到线程创建工厂。只不过这个线程工厂不再需要实现 ThreadFactory 接口,而是需要实现 ForkJoinWorkerThreadFactory 接口。后者是一个函数式接口,只需要实现一个名叫 newThread 的方法。

    2.2 在 ForkJoin 框架中有一个默认的 ForkJoinWorkerThreadFactory 接口实现DefaultForkJoinWorkerThreadFactory

  3. handler: 异常捕获处理器

    当执行的任务中出现异常,并从任务中被抛出时,就会被 handler 捕获。

  4. asyncMode: 异步模式

    4.1 asyncMode 参数表示任务是否为异步模式,其默认值为 false

    4.2 如果 asyncMode 为 true,表示子任务的执行遵循 FIFO (先进先出)顺序,并且子任务不能被合并(join);

    4.3 如果 asyncMode 为 false,表示子任务的执行遵循 LIFO (后进先出)顺序,并且子任务可以被合并(join)

Forkjoin默认构造

public ForkJoinPool() {
   
    static final int MAX_CAP = 0x7fff;        // max #workers - 1
    
    this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
         defaultForkJoinWorkerThreadFactory, null, false);
}

该构造函数的 parallelism 值为 CPU 核数;factory 值为 defaultForkJoinWorkerThreadFactory 默认的线程工厂;异常捕获处理器 handler 值为 null,表示不进行异常处理;异步模式 asyncMode 值为 false,使用 LIFO (后进先出)的、可以合并子任务的模式。