并发工具类之Fork-Join

本文探讨了分治法在归并排序中的应用,介绍了动态规划如何通过斐波那契数列示例展示递归推理,以及Fork-Join在任务并行处理中的工作原理和ForkJoinPool的实现。还涉及了一个实际案例,展示了如何使用ForkJoin实现4000长度数组的并行求和。
摘要由CSDN通过智能技术生成

一、分而治之

思想:

       将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

策略:

        对于一个规模为 n 的问题,若该问题可以容易地解决(比如说规模 n 较小)则直接解决,否则将其分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同(子问题相互之间有联系就会变为动态规划算法),递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

举例:归并排序(降序图示)

该算法是采用分治法的一个非常典型的应用。将已有序的子序列进行2-路归并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

二、动态规划

        需要前面的所有前序状态才能完成推理过程。我们将这一模型称为高阶马尔科夫模型。对应的推理过程叫做“动态规划法”。

        比如斐波那契数列 1 2 3 5 8 13 . . .

应用:爬楼梯可以一步一个台阶,也可以一步两个台阶,那么n级台阶有多少种爬法

问题分析:到达1级台阶是有一种,到达2级台阶有两种,到达3级台阶有3种,画图解释

图中 1⃣代表1级,2⃣代别2级,依次类推

        可以看出能到达6⃣级的只有4⃣级和5⃣级,那么只要知道到达4⃣级和5⃣级的有多少种爬法,然后相加就行了。

就是f(6)=f(5)+f(4)

同理f(5)=f(4)+f(3)

那么可以推出,f(n)=f(n-1)+f(n-2)

        这里不是在讲斐波那契数列,而是在讲它的分析过程。可以很明显的看出来,上一级的结果对下一级的计算是有影响的,这种就是动态规划。

三、Fork-Join

工作密取

        即当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 池中取出 Task 继续执行。

        ForkJoinPool 中维护着多个线程(一般为 CPU 核数)在不断地执行 Task,每个线程除了执行自己职务内的 Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的 Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高 CPU 利用率。

Fork/Join使用的标准范式

 四、Fork-Join API

ForkJoinPool 线程池,任务队列

        创建一个池子,默认开启的最大线程数等于服务器的核心数。每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。

        每个工作线程在运行中产生新的Task任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。

        每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。

        在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

  • invoke()执行任务,开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。和主线线程是同步的。
  • execute()执行任务,开启一个新线程,主线程和pool里的子线程是异步的,注意如果主线程使用join()方法会阻塞主线程
  • submit()执行任务,开启一个新线程,主线程和pool里的子线程是异步的,注意如果主线程使用join()方法会阻塞主线程

ForkJoinTask Task任务类

        代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。

  • RecusiveTask<T> 有返回值的任务,T就是返回值的类型
  • RecusiveAction 没有返回值的任务。

        我们需要继承上面两个抽象类,重写compute()方法,这个方法是进行任务再分裂或者进行结果计算。

        join() 等待该任务的处理线程处理完毕,获得返回值。需要注意的是join是阻塞的。

五、Fork-Join实现4000长度的随机数组求和,每个task求和计算的时候模拟业务请求1s

        把数组分割成一小段一小段的求和任务,并行执行,最后进行汇总。实现RecursiveTask,获取返回结果。

代码实现:

/**
 * ForkJoin执行累加
 */
public class SumArray {
    /**
     * 继承RecursiveTask, 实现compute()方法
     * 在compute()方法内进行判断是否再进行任务分割,或者获取结果
     */
    private static class SumTask extends RecursiveTask<Integer> {

        // 最小分割长度。length/最小长度
        private final static int THRESHOLD = MakeArray.ARRAY_LENGTH / 5;
        // 源数据
        private int[] src;
        // 起下标
        private int fromIndex;
        // 终下标
        private int toIndex;

        public SumTask(int[] src, int fromIndex, int toIndex) {
            this.src = src;
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }

        @Override
        protected Integer compute() {
            System.out.println("ThreadId: " + Thread.currentThread().getId() + " 是否再分割: " + !(toIndex - fromIndex < THRESHOLD) + " Task源数据 from:" + fromIndex + " to: " + toIndex);
            // 任务是否需要再次分割
            if (toIndex - fromIndex < THRESHOLD) {
                int count = 0;
                for (int i = fromIndex; i <= toIndex; i++) {
                    count = count + src[i];
                }

                try {
                    //模拟业务请求,休眠1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //返回结果
                return count;
            } else {
                //再次分割中值
                int mid = (fromIndex + toIndex) / 2;
                //创建左边Task任务
                SumTask left = new SumTask(src, fromIndex, mid);
                //创建右边Task任务
                SumTask right = new SumTask(src, mid + 1, toIndex);
                // 执行任务
                invokeAll(left, right);
                return left.join() + right.join();
            }
        }
    }

    public static void main(String[] args) {
        // 随机数组
        int[] src = MakeArray.makeArray();
        // 创建池forkjon池子,默认最大线程为服务器核心数
        // ForkJoinPool pool = new ForkJoinPool();
        // 可以指定最多使用多少个线程,这里未来效果,限制最大6个线程
        ForkJoinPool pool = new ForkJoinPool(6);

        // 创建task任务实例
        SumTask innerFind = new SumTask(src, 0, src.length - 1);
        // 开始时间
        long start = System.currentTimeMillis();
        pool.invoke(innerFind);
        //pool.execute(innerFind); 主线程和pool里的子线程是异步的,注意如果下面使用join()方法会阻塞主线程
        //pool.submit(innerFind); 主线程和pool里的子线程是异步的,注意如果下面使用join()方法会阻塞主线程

        // 获取结果
        Integer result = innerFind.join();
        System.out.println("求和结果 " + result
                + " 耗时:" + (System.currentTimeMillis() - start) + "ms");

    }

    static class MakeArray {
        //数组长度
        public static final int ARRAY_LENGTH = 4000;

        public static int[] makeArray() {
            //new一个随机数发生器
            Random r = new Random();
            int[] result = new int[ARRAY_LENGTH];
            for (int i = 0; i < ARRAY_LENGTH; i++) {
                //用随机数填充数组
                result[i] = r.nextInt(100);
            }
            return result;
        }
    }
}

 可以看到结果,使用了6个线程,总共创建了15个Task任务,其中分割了7次,即计算了8次。

缺:源码解析-池子底层是怎么实现的?工作密取怎么实现的?task是怎么分配到线程的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值