ForkJoinPool简介ForkJoinPool

ForkJoinPool

背景描述

过去我们在线程池解决问题时,通常维护了一个阻塞的任务队列。每个工作线程在任务完成后,就会去任务队列里面寻找任务。这种方式在我们执行数量较多且不互相依赖的任务时非常方便且高效。但是当我们需要执行一个很大的任务时,普通的线程池似乎就很难有什么帮助了。

在JDK7中新增了ForkJoinPool。ForkJoinPool采用分治+work-stealing的思想。可以让我们很方便地将一个大任务拆散成小任务,并行地执行,提高CPU的使用率。关于ForkJoinPool的精妙之处,我们将在后面的使用中慢慢说明。

如何使用

构造方法

Android官方文档中给出了三个构造方法。我们注意到在构造方法中,我们可以设置ForkJoinPool的最大工作线程数、工作线程工厂、拒绝任务的Handler和同步模式。

执行任务

ForkJoinPool提供了两套执行任务的API,它们的区别主要是返回的结果类型不同。invoke方法返回执行的结果,而submit方法返回执行的任务。

使用示例

需求

遍历系统所有文件,得到系统中文件的总数。

思路

通过递归的方法。任务在遍历中如果发现文件夹就创建新的任务让线程池执行,将返回的文件数加起来,如果发现文件则将计数加一,最终将该文件夹下的文件数返回。

代码实现

    CountingTask countingTask = new CountingTask(Environment.getExternalStorageDirectory());
    forkJoinPool.invoke(countingTask);

    class CountingTask extends RecursiveTask<Integer> {
        private File dir;

        public CountingTask(File dir) {
            this.dir = dir;
        }

        @Override
        protected Integer compute() {
            int count = 0;

            File files[] = dir.listFiles();
            if(files != null){
                for (File f : files){
                    if(f.isDirectory()){
                        // 对每个子目录都新建一个子任务。
                        CountingTask countingTask = new CountingTask(f);
                        countingTask.fork();
                        count += countingTask.join();

                    }else {
                        Log.d("tag" , "current path = "+f.getAbsolutePath());
                        count++;
                    }
                }
            }


            return count;
        }
    }         

原理说明

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

那么这样的工作模式,有什么好处呢?

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

上面的需求,如果我们用普通的线程池该如何完成?

如果我们使用newFixedThreadPool,当核心线程的路径下都有子文件夹时,它们会将路径下的子文件夹抛给任务队列,最终变成所有的核心线程都在等待子文件夹的返回结果,从而造成死锁。最终任务无法完成。

如果我们使用newCachedThreadPool,依然用上面的思路可以完成任务。但是每次子文件夹就会创建一个新的工作线程,这样消耗过大。

因此,在这样的情况下,ForkJoinPool的work-stealing的方式就体现出了优势。每个任务分配的子任务也由自己执行,只有自己的任务执行完成时,才会去执行别的工作线程的任务。

再来个例子

N项的Fibonacci数列求和,我们不再只能仰仗单个线程为我们执行任务。

package com.example;

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

public class MyClass {

    static int computeCount = 0;

    static class Fibonacci extends RecursiveTask<Integer> {
        int n;

        Fibonacci(int n) {
            this.n = n;
        }

        @Override
        protected Integer compute() {
            computeCount ++;
            System.out.printf("Current thread is " + Thread.currentThread()
                    + "\n n = " + n + "\n");

            if (n <= 2)
                return 1;
            Fibonacci f1 = new Fibonacci(n - 1);
            f1.fork();

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Fibonacci f2 = new Fibonacci(n - 2);
            f2.fork();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("wati temp answer is :" + n + "\n");
            int answer = f1.join() + f2.join();
            System.out.printf("temp answer is :" + answer  + ",  n is :" +n +"\n");
            return answer;
        }
    }


    public static void main(String[] args)  {
        ForkJoinPool pool = new ForkJoinPool(2);
        Fibonacci task = new Fibonacci(5);
        int answer = 0;
        answer = pool.invoke(task);
        System.out.printf("Hello answer is :" + answer +  " , compute count is :" + computeCount);
    }
}

结语

实测下来,当情况足够复杂时,ForkJoinPool的优势会愈加明显。但是,就像快排一样,最优策略并不是一个思路走到死,当分治的区域较小时,可以将小区域改用插入排序进行排序。同理,当我们递归到情况不再复杂时,就可以转而用别的线程池进行处理。

以上

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Kevin-ZhangCG

[ Java面试题 ]WEB篇

2048
来自专栏desperate633

深入理解Java多线程(multiThread)多线程的基本概念线程同步wait,notify,notifyAll线程的生命周期

一个java程序启动后,默认只有一个主线程(Main Thread)。如果我们要使用主线程同时执行某一件事,那么该怎么操作呢? 例如,在一个窗口中,同时画两排...

1262
来自专栏前端杂货铺

由objC运行时所想到的。。。

objC语言不仅仅有着面向对象的特点(封装,继承和多态),也拥有类似脚本语言的灵活(运行时),这让objC有着很多奇特的功能-可在运行时添加给类或对象添加方法,...

3708
来自专栏我的博客

PHP5.3~PHP5.5新特性汇总

一.PHP 5.3中的新特性 1. 支持命名空间 (Namespace) 2. 支持延迟静态绑定(Late Static Binding) 3. 支持got...

3678
来自专栏码神联盟

珍藏 | Java 岗位 100道 面试题及答案详解

7605
来自专栏大前端_Web

从前端模块化的概念来理解Webpack

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1373
来自专栏逸鹏说道

C#异步调用的方法

最经公司工作需要调用一个外部的webservice,同时要将传出的数据进行保存,以自己以前的习惯,就打算逐步操作,失败啊,完全没考虑过用户体验效果,在同事指点下...

3486
来自专栏Java技术

Java虚拟机值对象访问以及如何使用对象的引用(2)

对象访问在 Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及 Java 栈、 Java 堆、方法区这三个最重要内存区域之间的关联关...

661
来自专栏刺客博客

(转载)Linux之sed匹配第N个进行替换

1.6K4
来自专栏技术随笔

[Error] Segmentation fault

3776

扫码关注云+社区