美文网首页
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