Fork/Join框架—以分治的思想使用线程

前言

Fork/Join框架依赖于分治的思想,在了解Fork/Join之前需要先了解分治的思想和线程池技术(ThreadPoolExecutor)在分治思想下难以解决的问题

一、分治法

字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

举个例子,现在我们要计算从1累加到10000的值,利用分治法我们会这么解决,将1到10000之间的数字按照【最多计算1000个数的和】这一标准来划分成不同的“小问题”,即在每个组中求和,并将所有组的结果相加。下面的图很好地解释了这一过程:

在这里插入图片描述

转换成代码:

    @Test
    public void test1() {

        long num = 10000;
        long split = 1000;

        long res = calculate(1, num, split);

        System.out.println("结果是:" + res);

    }


    private long calculate(long begin, long end, long split) {
        long sum = 0;
        if (end - begin > split) {
            //如果大于任务分割值,则继续分治
            long rs1 = calculate(begin, (begin+end)/2, split);
            long rs2 = calculate((begin+end)/2+ 1, end, split);
            //解的合并
            return rs1 + rs2;

        } else {
            //如果小于任务分割值,则进行计算
            for (long i = begin; i <= end; i++) {
                sum += i;
            }
            return sum;
        }
    }

二、使用多线程的方式来提高分治法的效率

上面的例子有什么不好呢?
很明显,上面的例子是同步执行的,即先算 【1+…+625】 再算 【626+…+1250】 然后将两个结果相加,接着继续算 【1251+…+1875】 …。
这样就有可以提升的空间,我开启两个线程,一个计算【1+…+625】,一个计算【626+…+1250】,这样更快一些(单核CPU,暂时不考虑线程上下文切换的带来的效率损失),按照这种方式用ThreadPoolExecutor来提升下:

public class Task implements Callable<Long> {

    private long begin;

    private long end;

    private long limit;

    private ThreadPoolExecutor threadPoolExecutor;

    public Task(long begin, long end, long limit,ThreadPoolExecutor threadPoolExecutor) {
        this.begin = begin;
        this.end = end;
        this.limit = limit;
        this.threadPoolExecutor = threadPoolExecutor;
    }

    @Override
    public Long call() throws Exception {
        return calculate(begin, end, limit);
    }

    public long calculate(long begin, long end, long limit) throws ExecutionException, InterruptedException {
        long sum = 0;
        if (end - begin > limit) {
            //分治
            Future<Long> res1 = threadPoolExecutor.submit(new Task(begin, (begin+end)/2, limit,threadPoolExecutor));
            Future<Long> res2 = threadPoolExecutor.submit(new Task((begin+end)/2 + 1, end, limit,threadPoolExecutor));
            long res = 0;
            res = res1.get() + res2.get();
            return res;
        } else {
            for (long i = begin; i <= end; i++) {
                sum += i;
            }
            return sum;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //无界线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
            private AtomicLong num = new AtomicLong();

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "thread_test_" + num.getAndIncrement());
            }
        });

        long begin = System.currentTimeMillis();
        FutureTask<Long> task = new FutureTask<>(new Task(1, 10000, 1000,threadPoolExecutor));
        new Thread(task).start();

        //监控线程池线程数量
        while (true){
            System.out.println("当前线程池线程数量为:"+threadPoolExecutor.getPoolSize());
            if(task.isDone()){
                break;
            }
        }

        System.out.println("结果是:" + task.get());

        long end = System.currentTimeMillis();
        System.out.println("cost time:"+(end-begin));

    }
}

注意下,里面有这么行代码:

res = res1.get() + res2.get();

这里res1和res2线程会一直阻塞到其子任务线程任务结束才会结束,这样就会有一个比较难解决的问题,就是一旦子任务过多,会导致很多线程由于其子线程没有结束而一直阻塞,从而导致系统资源被大量占用。
以上文中的例子为例,假如在可以直接执行的子任务返回结果之前利用Thread.sleep方法模拟子任务的长时间运行,如下面的代码

    for (long i = begin; i <= end; i++) {
                sum += i;
            }
    //模拟子任务长时间运行
    Thread.sleep(20000);
            return sum;

我们可以发现,线程池中会有30个线程,其中16个线程做的任务是:

  for (long i = begin; i <= end; i++) {
                sum += i;
            }
    //模拟子任务长时间运行
    Thread.sleep(20000);
            return sum;

而其他的线程都在等子线程返回,这样会占用大量系统资源。下面一张图可以很好地解释这种现象:
在这里插入图片描述
即所有的任务都在等待其直系子任务全部返回结果。

三、Fork/Join框架如何利用多线程来处理分治代码

3.1、优点

Fork/Join框架的线程池在一个线程正在等待他创建的子线程运行的时候,如果当前线程如果完成了自己的任务后,就会寻找还没有被运行的任务并且运行他们,这样就是和Executors这个方式最大的区别,更加有效的使用了线程的资源和功能

3.2、基本类和接口

在这里插入图片描述

  • ForkJoinPool 线程池维护类,ForkJoinPool继承了AbstractExecutorService类
  • ForkJoinTast 任务抽象类
  • RecursiveTask 具体任务类,需要返回结果
  • RecursiveAction 具体任务类,无需返回结果

3.2.1、ForkJoinPool

  • execute(ForkJoinTask) 执行任务(不阻塞),无法获取任务返回值
  • submit(ForkJoinTask) 执行任务(不阻塞),可以获取任务返回值
  • invoke(ForkJoinTask) 执行任务(阻塞),可以获取任务返回值

3.2.2、RecursiveTask

  • T compute() 完成任务
  • V join() 返回计算结果
  • ForkJoinTask fork() 在当前任务正在运行的池中异步执行此任务
  • invokeAll(ForkJoinTask<?>… tasks) 将多个任务分配给其他线程,但是会留一个线程给自己

3.2.3、例子

还是上面提出的问题,我们来使用Fork/Join的方式解决

package com.lg;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * @description:
 * @author: lige
 * @create: 2019-01-12
 **/
public class Test3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CalculateTask task = new CalculateTask(1, 1000000000, 1000);

        ForkJoinPool pool = new ForkJoinPool();

        //一直阻塞到任务完成
        Long res = pool.invoke(task);
        System.out.println("res is " + res);

        //不会阻塞
        pool.submit(task);
        if (task.isDone()) {
            System.out.println("res2 is " + task.get());

        }


    }
}

class CalculateTask extends RecursiveTask<Long> {

    private long begin;

    private long end;

    private long split;

    public CalculateTask(long begin, long end, long split) {
        this.begin = begin;
        this.end = end;
        this.split = split;
    }

    @Override
    protected Long compute() {
        if (end - begin > split) {

            CalculateTask task1 = new CalculateTask(begin, (begin + end) / 2, split);

            CalculateTask task2 = new CalculateTask((begin + end) / 2 + 1, end, split);

            //这里应该使用invokeAll,使用下面的fork的方式,相当于当前线程把task1分配其他线程,把task2分配给其他线程,但是自己却没有任务执行
            invokeAll(task1,task2);
            //task1.fork();
            //task2.fork();

            return (task1.join() + task2.join());

        } else {
            long res = 0;

            for (long i = begin; i <= end; i++) {
                res += i;
            }
            return res;
        }
    }
}

3.3、使用场景

最佳应用场景:多核、多内存、可以分割计算再合并的计算密集型任务

3.4、一些最佳实践

  • 在实际应用时,使用多个ForkJoinPool是没有什么意义的。正是出于这个原因,一般来说来它实例化一次,然后把实例保存在静态字段,使之成为单例,这样就可以在软件中任何部分方便地重用了。
  • 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
  • 注意,在complete方法中如果需要将多个子任务分配给其他线程,需要调用invokeAll方法,不然当前线程将任务分配完成后,自己却没分配到任务,造成线程利用率低下,具体可以参考【https://www.dutycode.com/fork_join_1.html】这篇文章的解释

附:一些参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值