jdk8优化的工作窃取模式你敢尝试吗

1 什么是工作窃取模式

首先澄清一点,很多人认为工作窃取模式是JDK1.8的新特性,这不是正确的说法,而是在JDK 1.7 时,标准类库添加了 ForkJoinPool,作为对 Fork/Join 型线程池的实现,在JDK1.8时进行了优化,使之更好的被开发者所使用。

顾名思义,Fork 有 分叉 的意思,而 Join有 合并 的意思。ForkJoinPool 的功能即是如此:Fork 将大任务分割为多个小任务,然后让小任务执行,Join 是获得小任务的结果,然后进行合并,将合并的结果作为大任务的结果 —— 并且这会是一个递归的过程( 因为任务如果足够大,可以将任务多级分叉直到任务足够小,那么递归的临界值怎么指定呢,下面讲一个具体的使用)

2 算法分析

下面是《java并发编程的艺术》书中作图,非常一目了然,将一个大任务不断fork为子任务,然后子任务的结果集再不断join合并为大任务的结果
在这里插入图片描述

当有一个大任务(比较耗时,数据量比较大)时,可以把这个任务分割成若干互不依赖的子任务,为了减少线程竞争,把子任务分别放入不同队列,并为每个队列创建一个单独的工作线程
有的线程会提前把自己队列任务做完,为了提高效率,已经做完任务的线程会去其他队列窃取任务执行,以帮助其他未完成的线程(而不是自己等着)。此时他们访问同一个队列,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列。被窃取任务线程永远从双端队列头部取任务执行,窃取任务线程永远从双端队列尾部拿任务执行。其运行流程图如下:
在这里插入图片描述

Fork/Join 框架 与 线程池的区别

1 ThreadPoolExecutor 只能执行 Runnable 和 Callable 任务,而 ForkJoinPool 不仅可以执行 Runnable 和 Callable 任务,还可以执行 Fork/Join 型任务 —— ForkJoinTask —— 从而满足并行地实现分治算法的需要;

2 ThreadPoolExecutor 中任务的执行顺序是按照其在共享队列中的顺序来执行的,所以后面的任务需要等待前面任务执行完毕后才能执行而 ForkJoinPool 每个线程有自己的任务队列,并在此基础上实现了 Work-Stealing 的功能,使得在某些情况下 ForkJoinPool 能更大程度的提高并发效率。

3 对于一般的线程池实现 ,fork/join 框架的优势体现在对其中包含的任务的处理方式上在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行, 那么该线程会处于等待状态。

而在fork/join 框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题(窃取过来)来执行,这种方式减少了线程的等待时间,提高了性能

工作窃取算法优缺点
工作窃取算法优点:就是充分利用线程进行并行计算,减少了线程间的竞争。
工作窃取算法缺点:当双端队列里只有一个任务时,还时会存在竞争。而且该算法创建多个线程和多个双端队列,会消耗更过的系统资源。

3 代码示例

使用下面的代码计算 0,100000000的总和

package juc;

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

public class ForkJoinTest extends RecursiveTask<Integer> {
        private static final long THURSHOLD = 1000;      //临界值
        private static final long serialVersionUID = 6145975974734867182L;
        private int start;
        private int end;
        public ForkJoinTest(int start,int end) {
            this.start = start;
            this.end = end;
        }
        
        @Override
        protected Integer compute() {
            int length = end - start;
            //如果任务分割的足够小就计算任务
            if(length <= THURSHOLD) {
                int sum = 0;
                for(int i = start;i <= end;i++) {
                    sum+=i ;
                }
                return sum;
            } else {
            //任务大于阈值,分割成两个子任务
                int middle = (start + end) / 2;
           
                ForkJoinTest left = new ForkJoinTest(start, middle );
                left.fork();   //执行子任务

               
                ForkJoinTest right = new ForkJoinTest(middle + 1, end );
                right.fork();    //执行子任务
                return left.join() + right.join();   //等待子任务执行完并结果合并
            }
        }
        public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        //计算  0,100000000的总和
        ForkJoinTask<Integer> task = new ForkJoinTest(0,100000000);
        long sum = pool.invoke(task);
        System.out.println(sum);
    }
}

注意每个子任务在调用fork方法时,会再次进入compute方法,继续分割或者执行子任务并返回结果,调用join方法时等待子任务执行完成并得到其结果

大家注意到test类继承了RecursiveTask类,这是因为:

Fork/Join使用两个类来完成上述工作:
  - ForkJoinTask:要使用ForkJoin框架,必须先创建一个ForkJoin任务。它提供在任务中执行fork和join操作机制,在使用时,我们通常继承ForkJoinTask的子类
RecursiveAction或RecursiveTask,两者区别是RecursiveAction用于没有返回结果的任务,RecursiveTask用于有返回结果的任务
  - ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。被分割出来的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部窃取一个任务执行

那么如何知道任务的执行状态呢?

ForkJoinTask在执行时可能抛出异常,但我们无法再主线程里捕捉异常,但可以通过ForkJoinTask的isCompletedAbnnormally()方法来检查任务是否已经抛出异常或已经被取消,调用getException()可以获取异常,此方法返回Throwable对象,如果任务取消则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null。

4使用事项

选取一个合适的分割任务的临界值,对 ForkJoinPool 执行任务的效率有着至关重要的影响。
临界值选取过大,任务分割的不够细,则不能充分利用 CPU;
临界值选取过小,则任务分割过多,可能产生过多的子任务,导致过多的线程间的切换和加重 GC 的负担从而影响了效率。

所以,需要根据实际的应用场景选择一个合适的分割任务的临界值。

适用场景
当不同的任务的耗时相差比较大,即某些任务需要运行较长时间,而某些任务会很快的运行完成,这种情况下用 Work-Stealing 很合适;

但是如果任务的耗时很平均,则此时 Work-Stealing 并不适合,因为窃取任务时不同线程需要抢占锁,这可能会造成额外的时间消耗,而且每个线程维护双端队列也会造成更大的内存消耗。所以 ForkJoinPool 并不是 ThreadPoolExecutor 的替代品,而是作为对 ThreadPoolExecutor 的补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值