美文网首页
ForkJoinPool

ForkJoinPool

作者: From64KB | 来源:发表于2020-11-11 11:37 被阅读0次

    ForkJoinPool是什么?和ExectuorService有什么差异?
    ExectuorService相同的点在于,可以驱动并完成多个提交任务。特点如下:

    1. ForkJoinPool 适用于提交会产生子任务的任务。文字上似乎很难理解这是什么含义。看下面的图片:
      image.png

    这个提交的task本身会产生很多个子task(sub-task),那么这样的场景就是和使用ForkJoinPooltask会产生很多子task(sub-task),如果都在一个线程上完成的话,那么cpu将不能得到有效利用(为什么?参见这里关于CPU-线程模型的一些知识点)。说了这些还是感觉很抽象,那么有没有具体的例子可以作为理解什么样的task会产生多个子task(sub-task)呢?斐波那契数列(Fibonacci Sequence)。除了第一个和第二个数,每一个斐波那契数都可以被拆成前两个数的和。那么这样就可以将某个斐波那契数拆成前两个斐波那契数和的task。可以参考把原来需要递归调用的方法,放到多个线程去执行。

    如果还是不明白没,希望下面这张图能帮你理解ForkJoinPool 对于task本身会产生很多个子task(sub-task)这一概念。

    ForkJoinPool.png
    1. Per-Thread queueing & Working-Stealing 线程的任务队列和工作任务协同分担。最接近的翻译应该是: 线程对应的任务队列和工作任务盗取,显然这样翻译对于理解概念并没有什么额外帮助(当然第一种翻译也没有什么理解上的帮助...my poor English)。
      还是通过举例子来理解这一概念吧。假设有一个两个线程的线程池,向里面提交了很多task:
      image.png
      这两个线程本身就各自有一个deque (双端队列:double-ended queue)用于存储某个task fork出来的多个子task(并不会存储到上面的common queue)中:
      fork-task.png

    这样做有什么好处呢?

    • 线程只需要不停的从自己的deque中获取任务就行,提高了线程利用率,不需要停下来的从外部获取task,不会产生阻塞(例外:Working-Stealing时除外,这个稍后还会讲到
    • 由于减少了不同任务的线程调度,所以降低切换线程的性能损耗

    那这样做有没有什么问题呢?

    • 如果thread-1某个task拆分出了很多很多个子task,而 thread-2由于子task较少,很快就执行完了。那么thread-2就会看着thread-1一直满负载运行,而自己在摸鱼...
      这肯定是不够高效的,作为一个积极的打工人,thread-2应该勇敢的站出来替thread-1分担部分task。于是就产生了上面提到的Working-Stealing这一概念,可以说是工作偷取,或者工作协同。由于thread-2需要从thread-1deque尾部获取task,就会涉及到同步的问题,也必然会产生阻塞(上面提到的阻塞例外即来源于此)。

    综上,ForkJoinPoolExectuorService有什么差异?相同点都是用于异步并发执行任务,不同点在于ForkJoinPool的每个线程都有自己的 task deque用于存储某个task fork出来的子task。并且为了提高线程的利用率,ForkJoinPool各个线程之间存在子task协同完成这一概念,空闲的线程会通过获取其他线程deque尾部的子task协助完成任务。

    既然说的这么好,那怎么样使用呢? api和ExectuorService差别不大。但是需要关注submit(ForkJoinTask<T> task)、invoke(ForkJoinTask<T> task) 和 execute(ForkJoinTask<?> task)。还是拿上面斐波那契数列(Fibonacci Sequence)的例子:

            ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
            Integer invoke = forkJoinPool.invoke(new Fibonacci(10));
    
        static class Fibonacci extends RecursiveTask<Integer> {
    
            private int n;
    
            public Fibonacci(int n) {
                this.n = n;
            }
    
            @Override
            protected Integer compute() {
                if (n <= 1) {
                    return n;
                }
    
                Fibonacci fibonacci = new Fibonacci(n - 1);
                fibonacci.fork();//注意,fork出新的task
                Fibonacci fibonacci1 = new Fibonacci(n - 2);
                fibonacci1.fork();//注意,fork出新的task
    
                return fibonacci.join() + fibonacci1.join();//join获取结果
            }
        }
    

    也很简单,那么ForkJoinPool使用有什么注意点呢?

    • 避免在task中出现同步相关的代码
    • 避免在不同的task间分享同一个变量
    • 不要在代码中出现I/O这样会Block的操作
    • 每个task要相对单一和独立

    不难看出ForkJoinPool适用于CPU敏感型的操作需求,注重执行效率。

    相关文章

      网友评论

          本文标题:ForkJoinPool

          本文链接:https://www.haomeiwen.com/subject/tfidbktx.html