Java 多线程中的任务分解机制-ForkJoinPool,以及CompletableFuture

简介

ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。

Java7 提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。

ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。

使用方法:创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务了。

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

ForkJoinTask:

RecusiveAction实现方法:

package com.qhong.thread.ForkJoinPoolDemo;

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;


public class ForkJoinPoolDemo extends RecursiveAction {

    private static final long serialVersionUID = 1L;
    //定义一个分解任务的阈值——50,即一个任务最多承担50个工作量
    private int THRESHOLD=50;
    //任务量
    private int task_Num=0;
    
    ForkJoinPoolDemo(int Num){
        this.task_Num=Num;
    }
    
    public static void main (String[] args) throws Exception {
        //创建一个支持分解任务的线程池ForkJoinPool
        ForkJoinPool pool=new ForkJoinPool();
        ForkJoinPoolDemo task=new ForkJoinPoolDemo(120);

        pool.submit(task);
        pool.awaitTermination(20, TimeUnit.SECONDS);//等待20s,观察结果
        pool.shutdown();
    }

    /**
     * @author qhong
     * @param 
     * @return 
     * @date 2018/4/18 17:13
     * @description 实现recursiveAction中抽象方法
     */
    @Override
    protected void compute() {
        if(task_Num<=THRESHOLD){
            System.out.println(Thread.currentThread().getName()+"承担了"+task_Num+"份工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            //随机解成两个任务
            Random m=new Random();
            int x=m.nextInt(50);

            ForkJoinPoolDemo left=new ForkJoinPoolDemo(x);
            ForkJoinPoolDemo right=new ForkJoinPoolDemo(task_Num-x);

            left.fork();
            right.fork();
        }
    }
}

Output:

ForkJoinPool-1-worker-1承担了6份工作
ForkJoinPool-1-worker-2承担了2份工作
ForkJoinPool-1-worker-3承担了30份工作
ForkJoinPool-1-worker-0承担了9份工作
ForkJoinPool-1-worker-1承担了46份工作
ForkJoinPool-1-worker-2承担了17份工作
ForkJoinPool-1-worker-0承担了0份工作
ForkJoinPool-1-worker-3承担了10份工作

RecusiveTask的具体实现:

package com.qhong.thread.ForkJoinPoolDemo;

import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

/**
 * @author qhong
 * @date 2018/4/18 16:14
 * @description
 **/
public class ForkJoinCalculator implements Calculator {
    private ForkJoinPool pool;

    public ForkJoinCalculator() {
        // 也可以使用公用的 ForkJoinPool:
        // pool = ForkJoinPool.commonPool()
        pool = new ForkJoinPool();
    }

    public static void main(String[] args) {
        ForkJoinCalculator forkJoinCalculator=new ForkJoinCalculator();
        long[] numbers=LongStream.range(1,20).toArray();
        System.out.println(Arrays.toString(numbers));
        long result=forkJoinCalculator.sumUp(numbers);
        System.out.println("result:"+result);
    }

    private static class SumTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int from;
        private int to;

        public SumTask(long[] numbers, int from, int to) {
            this.numbers = numbers;
            this.from = from;
            this.to = to;
        }

        @Override
        protected Long compute() {
            // 当需要计算的数字小于6时,直接计算结果
            if (to - from < 4) {
                long total = 0;
                for (int i = from; i <= to; i++) {
                    total += numbers[i];
                }
                System.out.println(String.format("currentThread:%s,total:%s,from:%s,to:%s",Thread.currentThread().getName(),total,from,to));

                return total;
                // 否则,把任务一分为二,递归计算
            } else {
                int middle = (from + to) / 2;
                SumTask taskLeft = new SumTask(numbers, from, middle);
                SumTask taskRight = new SumTask(numbers, middle+1, to);
                taskLeft.fork();
                taskRight.fork();
                return taskLeft.join() + taskRight.join();
            }
        }
    }

    @Override
    public long sumUp(long[] numbers) {
        return pool.invoke(new SumTask(numbers, 0, numbers.length-1));
    }
}

Output:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
currentThread:ForkJoinPool-1-worker-2,total:6,from:0,to:2
currentThread:ForkJoinPool-1-worker-3,total:36,from:10,to:12
currentThread:ForkJoinPool-1-worker-2,total:9,from:3,to:4
currentThread:ForkJoinPool-1-worker-3,total:29,from:13,to:14
currentThread:ForkJoinPool-1-worker-2,total:21,from:5,to:7
currentThread:ForkJoinPool-1-worker-3,total:70,from:15,to:18
currentThread:ForkJoinPool-1-worker-2,total:19,from:8,to:9
result:190

分析:

根据上面的示例代码,可以看出 fork() 和 join() 是 Fork/Join Framework “魔法”的关键。我们可以根据函数名假设一下 fork() 和 join() 的作用:

  • fork():开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。
  • join():等待该任务的处理线程处理完毕,获得返回值。

并不是每个 fork() 都会促成一个新线程被创建,而每个 join() 也不是一定会造成线程被阻塞。

Fork/Join Framework 的实现算法并不是那么“显然”,而是一个更加复杂的算法——这个算法的名字就叫做 work stealing 算法。

  • ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
  • 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
  • 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
  • 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
  • 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

fork() 做的工作只有一件事,既是把任务推入当前工作线程的工作队列里。

join() 的工作则复杂得多,也是 join() 可以使得线程免于被阻塞的原因——不像同名的 Thread.join()。

  1. 检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。
  2. 查看任务的完成状态,如果已经完成,直接返回结果。
  3. 如果任务尚未完成,但处于自己的工作队列内,则完成它。
  4. 如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式),执行,以期帮助它早日完成欲 join 的任务。
  5. 如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务。
  6. 递归地执行第5步。

img

所谓work-stealing模式,即每个工作线程都会有自己的任务队列。当工作线程完成了自己所有的工作后,就会去“偷”别的工作线程的任务。

假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

submit:

其实除了前面介绍过的每个工作线程自己拥有的工作队列以外,ForkJoinPool 自身也拥有工作队列,这些工作队列的作用是用来接收由外部线程(非 ForkJoinThread 线程)提交过来的任务,而这些工作队列被称为 submitting queue 。

submit() 和 fork() 其实没有本质区别,只是提交对象变成了 submitting queue 而已(还有一些同步,初始化的操作)。submitting queue 和其他 work queue 一样,是工作线程”窃取“的对象,因此当其中的任务被一个工作线程成功窃取时,就意味着提交的任务真正开始进入执行阶段。

ForkJoinPool与ThreadPoolExecutor区别:

1.ForkJoinPool中的每个线程都会有一个队列,而ThreadPoolExecutor只有一个队列,并根据queue类型不同,细分出各种线程池

2.ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,ThreadPoolExecutor中根本没有什么父子关系任务

3.ForkJoinPool在使用过程中,会创建大量的子任务,会进行大量的gc,但是ThreadPoolExecutor不需要,因此单线程(或者任务分配平均)

4.ForkJoinPool在多任务,且任务分配不均是有优势,但是在单线程或者任务分配均匀的情况下,效率没有ThreadPoolExecutor高,毕竟要进行大量gc子任务

ForkJoinPool在多线程情况下,能够实现工作窃取(Work Stealing),在该线程池的每个线程中会维护一个队列来存放需要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。

ThreadPoolExecutor因为它其中的线程并不会关注每个任务之间任务量的差异。当执行任务量最小的任务的线程执行完毕后,它就会处于空闲的状态(Idle),等待任务量最大的任务执行完毕。

因此多任务在多线程中分配不均时,ForkJoinPool效率高。

stream中应用ForkJoinPool

 Arrays.asList("a1", "a2", "b1", "c2", "c1")
                .parallelStream()
                .filter(s -> {
                    System.out.format("filter: %s [%s]\n",
                            s, Thread.currentThread().getName());
                    return true;
                })
                .map(s -> {
                    System.out.format("map: %s [%s]\n",
                            s, Thread.currentThread().getName());
                    return s.toUpperCase();
                })
                .sorted((s1, s2) -> {
                    System.out.format("sort: %s <> %s [%s]\n",
                            s1, s2, Thread.currentThread().getName());
                    return s1.compareTo(s2);
                })
                .forEach(s -> System.out.format("forEach: %s [%s]\n",
                        s, Thread.currentThread().getName()));

parallelStream让部分Java代码自动地以并行的方式执行

最后:

有一点要注意,就是手动设置ForkJoinPool的线程数量时,实际线程数为设置的线程数+1,因为还有一个main主线程

即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。

与ForkJoinPool对应的是CompletableFuture

Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。

阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU资源,而且也不能及时地得到计算结果

CompletableFuture就是利用观察者设计模式当计算结果完成及时通知监听者

在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

具体讲解链接:http://colobu.com/2016/02/29/Java-CompletableFuture/

http://colobu.com/2018/03/12/20-Examples-of-Using-Java%E2%80%99s-CompletableFuture/

参考:

http://www.cnblogs.com/lixuwu/p/7979480.html#undefined

http://blog.dyngr.com/blog/2016/09/15/java-forkjoinpool-internals/

https://www.jianshu.com/p/8d7e3cc892cf

https://blog.csdn.net/dm_vincent/article/details/39505977

posted @ 2022-09-20 14:41  hongdada  阅读(1351)  评论(0)    收藏  举报