[笔记][Java7并发编程实战手册]5.Fork\Join(Java1.7新特性)框架

[笔记][Java7并发编程实战手册]系列目录


Fork/Join框架

本掌包含:
1. 创建Fork/Join线程池
2. 合并任务的结果
3. 异步运行任务
4. 在任务中抛出异常
5. 取消任务


5.1简介

  一般在开发简单的并发程序时,会使用Runnable对象,然后创建对应的Thread对象来控制程序中这些线程的创建、执行以及线程的状态。Java1.5引入了ExecutorFramework(Executor和ExecutorService接口)
  ExecutorFramework:执行器框架将任务的创建和执行进行了分离,通过这个框架只需要实现Runnable接口的对象和使用Executor对象,然后将Runnable对象发送给执行器,执行器再负责这些任务锁需要的线程,包括线程的创建管理已经线程的结束
  Java1.7更进了一步,包括了ExecutorService接口的另一种实现,来解决特殊类型的问题,Fork/Join,也称为分解/合并框架。
  Fork/Join框架用来解决分治技术,将问题拆分成若干小任务,在一个任务中,先检查要解决任务的问题的大小,决定是否再拆分任务。如果不需要拆分任务了,就在当前任务中解决问题。然后根据需要返回任务的结果。ForkJoinPool类看作一个特殊的Executor执行器类型,这个框架包括以下两个操作:
1. 分解(Fork)操作:当需要将一个任务拆分成更小的多个任务时,在框架中执行这些任务
2. 合并(Join)操作:当一个主任务等待其创建的多个子任务的完成执行。


  Fork/Join 和 ExecutorFramework 主要的区别在于 工作窃取算法(Work-StealingAlgroithms),使用Join操作让一个主任务等待它所创建的子任务的完成,执行这个任务的线程称之为 工作者线程(worker Thread),工作者线程会寻找其他任未被执行的任务,然后开始执行。
  什么是工作窃取算法?:就是指某个线程从其他队列里窃取任务来执行。从而提升了性能。

为了达到以上的效果,Fork/Join框架有以下几点限制:

  1. 任务只能使用fork() 和 join() 操作当同步机制。如果使用其他的同步机制,工作线程就不能执行其他的任务,当然这些任务是在同步操作里时。比如在Fork/Join框架中将一个任务休眠,正在执行这个任务的工作者线程在休眠期内不能执行另一个任务。
  2. 任务不能执行I/O操作,比如文件数据的读取与写入
  3. 任务不能抛出非运行时异常(Checke Exception),必须在代码中处理这些异常

Fork/Join框架的核心是由ForkJoinPool和ForkJoinTask组成

  1. ForkJoinPool : 这个类实现了 ExecutorServic接口和工作窃取算法。它管理工作者线程,并提供任务的状态信息,以及任务的执行信息
  2. ForkJoinTask :是一个将在ForkJoinPool中执行的任务的基类

Fork/Join框架提供了在一个任务里执行fork和join操作的机制和控制任务状态的方法,通常,为了实现Fork/Join任务,需要实现以下两个类之一的子类。

  1. RecursiveAction :用于任务没有返回结果的场景
  2. RecursiveTask :用于任务有返回结果的场景


5.2.创建Fork/Join线程池

本小节将学习:如何创建ForkJoinPool线程池和执行RecursiveAction任务。

  1. ForkJoinPool : 一个线程池执行器,由于本人英文不好,看不懂api文档。解释不了更多的信息
  2. RecursiveAction : 是一个ForkJoinTask任务类,递归无结果的任务类。类似callable一样的线程任务,该类的使用,是按照jdkApi文档的范例编写。
  3. 书上说 执行主任务的线程叫做工作者线程(worker),但是通过测试,发现这个里面的线程都是worker1,2这样的。所以说就是没有子任务咯? 全是主任务咯?,如果这样来理解那就错了:原来是相对的 主 和子。请看第工作原理
  4. execute(Runnable task) :这个发送的是一个runnable,需要注意的是,使用Runnable不采用工作窃取算法,只有使用execute(ForkJoinTask

示例

场景描述:以下示例描述了,批量修改很多商品的价格,使用Fork/Join线程池 和 RecursiveAction(ForkJoinTask)来实现 递归的分配任务执行的一个示例

/**
 * Created by zhuqiang on 2015/9/6 0006.
 */
public class Client {
    public static void main(String[] args) throws InterruptedException {
        List<Product> list = new ArrayList<Product>();
        for (int i = 0; i < 40; i++) {  //生成商品数据
            Product p = new Product("苹果"+i,10);
            list.add(p);
        }

        ForkJoinPool fjp = new ForkJoinPool();

        Task task = new Task(list, 0, list.size(), 19);
        fjp.execute(task);
//        fjp.shutdown(); //关闭线程池
//        fjp.awaitTermination(1, TimeUnit.MINUTES); //等待超时。结合shutdown来让任务一完成就继续执行下面的代码

        //为了能看到更多的任务信息,就不使用上面的等待任务结束方法了。使用循环的方式来查看任务的信息
        do {
            System.out.printf("正在活动的线程数量:%s,这一个参数不知道是什么意思,英文不好:%s,并行执行的最大数量:%s\n",fjp.getActiveThreadCount(),fjp.getStealCount(),fjp.getParallelism());

        }while (!task.isDone()); //如果任务还未完成,则继续获取信息

        if(task.isCompletedNormally()){ //如果这个任务完成没有抛出异常并没有取消。
            System.out.println("main:任务完成");
        }
        System.out.println("main:------------------------------  打印任务结果");
        for (Product product : list) {
            int price = product.getPrice();
            String name = product.getName();
            if(price != 19){ //结果不是所期望的。就打印出来
                System.out.println(name + "," + price);
            }
        }
        System.out.println("main:------------------------------  打印任务结束");

    }
}

/**
 * 任务类
 */
class Task extends RecursiveAction{
    private List<Product> list; //所有任务
    private int start; //处理任务的开始索引
    private int end; //处理任务的结束索引
    private int price;  //更改的价格

    public Task(List<Product> list, int start, int end, int price) {
        this.list = list;
        this.start = start;
        this.end = end;
        this.price = price;
    }

    @Override
    protected void compute() {
        if(end - start <= 10){ //每个task 只能处理10条数据。
            System.out.printf("起始:start:%s,end:%s\n",start,end);
            update();
        }else{ //多余的数据,则需要分给更多的任务
            int middle = (end+start) / 2;  // 因为是索引。所以需要开始和结尾相加,然后除以2 就能得到 两个索引之间的数值
            Task task1 = new Task(list,start,middle,19);
            Task task2 = new Task(list,middle,end,19);
            System.out.printf("分析:middle:%s,start:%s,end:%s\n",middle,start,end); //方便推算
            invokeAll(task1,task2);  //这里把任务分成了2半递归执行。
        }
    }

    // 根据给定的起始索引和结束索引更新结果
    private void update(){
        for (int i = start; i < end; i++) {
            Product product = list.get(i);
            product.setPrice(price);
            //下面的信息,适合在商品数量少的情况下打开
            //System.out.printf("%s,修改了价格,索引:%s,%s,%s\n", Thread.currentThread().getName(), i,product.getName(),product.getPrice() );
        }

    }
}

// 商品类
class Product{
    private String name;
    private int price;

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

某一次的运行结果:

分析:middle:20,start:0,end:40
正在活动的线程数量:1,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
分析:middle:10,start:0,end:20
分析:middle:30,start:20,end:40
正在活动的线程数量:2,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
正在活动的线程数量:4,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
正在活动的线程数量:4,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
正在活动的线程数量:4,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
正在活动的线程数量:4,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
起始:start:20,end:30
起始:start:30,end:40
起始:start:0,end:10
起始:start:10,end:20
正在活动的线程数量:4,这一个参数不知道是什么意思,英文不好:0,并行执行的最大数量:8
main:任务完成
main:------------------------------  打印任务结果
main:------------------------------  打印任务结束

结果分析:

  1. 查看起始信息:我们有40个商品,每个任务处理10个商品。刚好4个工作线程处理。
  2. 查看分析信息:去中间索引,这个分析在商品数量不能被2整除的时候很有用,因为在不能被2整除的情况下,该示例任然会尽可能的均衡分配任务的数量,(你可以试试把商品数量改成50)

工作原理

  1. invokeAll方法来执行一个主任务锁创建的多个子任务,这个是一个同步的调用,主任务将等待子任务的完成,然后继续执行(有可能是结束),当这个主任务等待它的子任务时,执行这个主任务的工作者线程接收另一个等待执行的任务并开始执行(并行),正因为有了这个行为,所以说Fork/Join框架提供了一种比Runnable和Callable对象更加高效的任务管理机制。
  2. ForkJoinTask类的invokeAll方法是执行器框架ExecutorFramework和Fork/Join框架之间的主要差异之一。在执行器框架中。
    在执行器框架中:所有的任务必须发送给执行器
    在Fork/Join框架(本示例中):线程池中包含了待执行方法的任务,任务的控制也是在线程池中进行的,我们在task类中使用了invokeAll方法,task类继承了RecursiveAction,而RecursiveAction类则继承了ForkJoinTask。可以看下面的类图;
    这里写图片描述
    我们发送一个唯一的任务到线程池中,通过使用execute方法来更新所有产品的列表

5.3.合并任务的结果

本小结将学习如何使用Fork/Join来合并任务的结果,上一小节,是使用的没有结果的RecursiveAction来执行。
与5.2章节其实使用方式差不多。还是使用 jdk中的标准拆分任务的模版代码;只是有了返回结果。

//jdk中的模版拆分代码
class Fibonacci extends RecursiveTask<Integer> {
   final int n;
   Fibonacci(int n) { this.n = n; }
   Integer compute() {
     if (n <= 1)
        return n;
     Fibonacci f1 = new Fibonacci(n - 1);
     f1.fork();
     Fibonacci f2 = new Fibonacci(n - 2);
     return f2.compute() + f1.join();
   }
 }
  1. RecursiveTask : 用于任务有返回结果的场景
  2. RecursiveTask.join() : 也是用来获取任务的合并结果。和 get() 效果上差不多。英文不行。看不懂api。没办法
  3. RecursiveTask.get(long timeout,TimeUnit unit) : 该方法,是给定一个指定的超时时间,如果超时还没有返回结果则返回null
  4. invokeAll(task1,task2): 是一个同步的方法,任务会被挂起,等待子任务发送到线程池中并且直到完成

示例

场景描述:在 [笔记][Java7并发编程实战手册]3.5 在集合点的同步CyclicBarrier循环barrier 章节中使用了分治编程技术。来实现了一个在矩阵中查询数字的示例,下面还将使用这个矩阵示例,只不过把它改写成了用 Fork/Join框架来实现

// 生成矩阵的类,这个类没有变化。还是之前的类
/**
 * Created by zhuqiang on 2015/8/22 0022.
 * 随机矩阵
 */
public class MatrixMock {
    private int[][] data;

    /**
     *
     * @param size 矩阵行数
     * @param cols 矩阵列数
     * @param number 要寻找的数字
     */
    public MatrixMock(int size, int cols, int number) {
        data = new int[size][cols];
        Random random = new Random();

        int counter = 0;
        //  用随机数为矩阵赋值。每生成一个字,就用它跟要查找的数字比较,进行比较。如果一致,就用计数器加1
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < cols; j++) {
                data[i][j] = random.nextInt(10);
                if(data[i][j] == number){
                    counter++;
                }
            }
        }
        //用来验证多线程查找的正确性
        System.out.printf("在矩阵中找到了数字:%d,%d次\n",number,counter);
//        测试的时候,可以放开此代码,能打印出 矩阵分布图。当然需要矩阵10 * 10 比较小的收,控制台才能装得下
//        for (int i = 0; i < data.length; i++) {
//            for (int j = 0; j < data[i].length; j++) {
//                System.out.printf(data[i][j] + " | ");
//            }
//            System.out.println("");
//        }
    }

    /**
     * 返回指定矩阵中的行数据
     * @param row 行号
     * @return
     */
    public int[] getRow(int row){
        if(row >= 0 && row < data.length){
            return data[row];
        }
        return  null;
    }
}

//本节的示例类
/**
 * Created by zhuqiang on 2015/9/7 0007.
 */
public class Clinet {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        final int rows = 10000;  // 矩阵行数
        final int cols = 10000;  //矩阵列数
        final int number = 5;  //查找的数字

        long start = System.currentTimeMillis();
        MatrixMock mock = new MatrixMock(rows,cols,number);  //生成矩阵对象
        long end = System.currentTimeMillis();
        System.out.println("创建矩阵花费时间:" + (end - start));


        ForkJoinPool pool = new ForkJoinPool();
        Task task = new Task(mock, 0, rows, 5);

        start = System.currentTimeMillis();
        pool.execute(task);

        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MILLISECONDS);
        System.out.println("线程搜索的结果是:" + task.get());

        end = System.currentTimeMillis();
        System.out.println("线程搜索时间是:" + (end - start));


        start = System.currentTimeMillis();
        int temp = 0;
        for (int i = 0; i < rows; i++) {
            int[] rs = mock.getRow(i);
            for (int row : rs) {
                if(5 == row){
                    temp++;
                }
            }
        }
        end = System.currentTimeMillis();
        System.out.println("单线程搜索结果是:" + temp);
        System.out.println("单线程搜索时间是:" + (end - start));
    }
}

//任务类。查找数字出现的次数
class Task extends RecursiveTask<Integer>{
    private MatrixMock mock;
    private int start; //查询起始行索引
    private int end; //查询结束行索引
    private int num;  //要查找的数字

    public Task(MatrixMock mock, int start, int end,int num) {
        this.mock = mock;
        this.start = start;
        this.end = end;
        this.num = num;
    }

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

        if(end - start < 100){ //每个任务最多负责5行数据
            result = this.search();
            //适合矩阵小的时候 查看对比结果
           // System.out.printf("%s,搜索起始行是:%s-%s,搜索结果是:%s\n",Thread.currentThread().getName(),start,end,result);
        }else{  //否则则拆分成两个子任务
            int mid = (end + start)/2;
            Task task1 = new Task(mock, start, mid,num);
            Task task2 = new Task(mock, mid, end,num);
            invokeAll(task1,task2);
            try {
                result = task1.get() + task2.get();  //两个结果相加,要想到 该框架的特性就是 递归
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    //计算当前任务分配的行数
    private int search(){
        int result = 0;
        for (int i = start; i < end; i++) {
            int[] rows = mock.getRow(i);
            for (int row : rows) {
                if(num == row){
                    result++;
                }
            }
        }
        return result;
    }
}

某一次的运行结果:

在矩阵中找到了数字:5,10002307次
创建矩阵花费时间:1619
线程搜索的结果是:10002307
线程搜索时间是:42
单线程搜索结果是:10002307
单线程搜索时间是:214

结果说明

  1. 我们使用了 10000 * 10000 的矩阵来随机生成一些数字。
  2. 统计了3个时间,矩阵生成,Fork/Join框架搜索结果时间,普通单线程时间;
  3. 根据结果可以看到,后面两种结果 都和矩阵的标准答案相符合,而在时间上,可以看出大大的提高了性能

5.4.异步运行任务

   本小节将学习 怎么异步运行任务。在上一节中说道使用 invokeAll(task1,task2); 是同步方式,该方式会将当前任务挂起直到子任务发送到 Fork/join线程池中执行完成,这种方式允许工作窃取算法,分配一个新任务给在执行休眠任务的工作者线程。相反当采用异步的方式(比如,fork()),任务将继续执行,所以就没有办法使用工作窃取算法了。因为不存在等待的线程了。除非使用join() 或则 get() 来获取结果,等待任务的完成。

  1. invokeAll()采用同步方式,工作者线程将会休眠等待子任务的完成,所以能使用窃取算法派给工作者一个新任务
  2. fork() 采用异步方式,只有结合join()或者get() 来等待任务的完成,进而可以使用窃取算法来提高性能。
  3. 关于最大并行任务数量,不知道是否跟cpu有关。我的cpu是4核8线程的,是否就是8并行任务呢
  4. get():如果ForkJoinTask类执行结束,或则一直等到结束,那么get()方法的这个版本则返回由compute()方法返回的结果
  5. get() 方法 和 join()方法的区别:
    1. join()方法不能被中断,如果中断join()方法的线程,方法将抛出Interrupted异常
    2. 如果任务抛出任何运行时异常,那么get()方法将返回ExecutionException异常,但是join方法返回的是RuntimeException;

示例

场景描述:使用异步任务的方式,获取一个文件夹中的指定后缀,如果文件夹中有子目录,则异步拆分一个子任务给它执行这个子目录,最后返回匹配的文件名称

/**
 * Created by zhuqiang on 2015/9/8 0008.
 */
public class Client {
    public static void main(String[] args) throws InterruptedException {
        ForkJoinPool pool = new ForkJoinPool();
        Task mp3 = new Task("D:\\downLoad\\Music", "mp3");
        pool.execute(mp3);

        do {
            System.out.println("***********   状态信息巡查 ***************");
            System.out.printf("最大并行任务:%s,当前活动任务数(不准确的):%s,队列中的任务数量:%s,窃取数量:%s\n", pool.getParallelism(), pool.getActiveThreadCount(), pool.getQueuedTaskCount(), pool.getStealCount());
            TimeUnit.MILLISECONDS.sleep(10); 
        } while (!mp3.isDone()); //未完成则一直循环获取状态信息

        pool.shutdown();
        List<String> join = mp3.join();
        System.out.println("共找到符合的文件数量:" + join.size());
/*        for (String s : join) {
            System.out.println(s);
        }*/

    }
}

class Task extends RecursiveTask<List<String>>{
    private String path;  //文件夹路径
    private String suffix; //后缀

    public Task(String path, String suffix) {
        this.path = path;
        this.suffix = suffix;
    }

    @Override
    protected List<String> compute() {
        List<String> result = new ArrayList<String>(); //存储结果
        List<Task> tasks = new ArrayList<Task>();  //存储任务

        File file = new File(path);
        File[] files = file.listFiles();
        for (File f : files) { //分发和执行任务
            if(f.isDirectory()){ //如果是文件夹,则使用异步的方式发送一个任务去执行
                Task task = new Task(f.getAbsolutePath(), suffix);
                task.fork(); //拆分任务异步执行
                tasks.add(task);
            }else {
                String name = f.getName();
                if(name.endsWith(suffix)){
                    result.add(name);
                }
            }
        }
        if(tasks.size() > 1) { //如果当前任务大于1个 则打印信息
            System.out.printf("%s,tasks size(当前路径有) = %s个(文件夹),当前路径是:%s\n", Thread.currentThread().getName(), tasks.size(), path);
        }
        for (Task task : tasks) {  // 获取当前任务的结果
            List<String> join = task.join();  //调用join方法等待任务完成
            result.addAll(join); //把任务结果添加到当前任务的结果中
        }

        return result;
    }
}

某一次的执行结果:

***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):1,队列中的任务数量:0,窃取数量:0
ForkJoinPool-1-worker-7,tasks size(当前路径有) = 4个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\01
ForkJoinPool-1-worker-0,tasks size(当前路径有) = 3个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\00
ForkJoinPool-1-worker-6,tasks size(当前路径有) = 6个(文件夹),当前路径是:D:\downLoad\Music\iTunes\iTunes Media
ForkJoinPool-1-worker-5,tasks size(当前路径有) = 15个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F
ForkJoinPool-1-worker-3,tasks size(当前路径有) = 7个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork
ForkJoinPool-1-worker-2,tasks size(当前路径有) = 3个(文件夹),当前路径是:D:\downLoad\Music\iTunes
ForkJoinPool-1-worker-0,tasks size(当前路径有) = 5个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\02
ForkJoinPool-1-worker-2,tasks size(当前路径有) = 7个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\03
ForkJoinPool-1-worker-0,tasks size(当前路径有) = 4个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\07
ForkJoinPool-1-worker-2,tasks size(当前路径有) = 4个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\09
ForkJoinPool-1-worker-11,tasks size(当前路径有) = 3个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\08
ForkJoinPool-1-worker-0,tasks size(当前路径有) = 5个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\10
ForkJoinPool-1-worker-7,tasks size(当前路径有) = 3个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\11
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):9,队列中的任务数量:10,窃取数量:39
ForkJoinPool-1-worker-2,tasks size(当前路径有) = 2个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\13
ForkJoinPool-1-worker-6,tasks size(当前路径有) = 2个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\12\07
ForkJoinPool-1-worker-11,tasks size(当前路径有) = 3个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\14
ForkJoinPool-1-worker-10,tasks size(当前路径有) = 4个(文件夹),当前路径是:D:\downLoad\Music\iTunes\Album Artwork\Cache\767F615B8344D13F\15
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):2,队列中的任务数量:0,窃取数量:60
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):2,队列中的任务数量:0,窃取数量:60
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):2,队列中的任务数量:0,窃取数量:60
ForkJoinPool-1-worker-1,tasks size(当前路径有) = 5个(文件夹),当前路径是:D:\downLoad\Music
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):1,队列中的任务数量:0,窃取数量:60
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):1,队列中的任务数量:0,窃取数量:60
***********   状态信息巡查 ***************
最大并行任务:8,当前活动任务数(不准确的):1,队列中的任务数量:0,窃取数量:60
共找到符合的文件数量:731

结果说明:

  1. 我给定的目录是一个MP3的目录。
  2. 看到上面的状态。窃取数量,和队列的任务数量,不去研究源码还真不知道有什么关联。看介绍说是,工作者线程会窃取队列中的任务进行执行。从而提高性能。。

5.5.在任务中抛出异常

  本小节将学习到怎么在task中抛出异常。怎么获取这些异常。
Java有两种类型的异常:

  1. 非运行时异常(Checked Exception):必须在方法上通过throws 子句抛出,或则通过try…catch语句进行扑捉处理。
  2. 运行时异常(Unchecked Exception):不是强制的需要捕捉处理和throws抛出。

在ForkJoinTask类的compute方法中不能抛出非运行时异常,因为该方法没有throws的声明,根据Java重新方法的规则,所以不能抛出。而且在该compute中抛出的运行时异常,给我最明显直观的结果是,只要不调用get()获取结果,控制台是不会打印异常信息的。也就是说,异常被吞噬了。但是我们可以通过该类的其他方法来获取该异常。

  1. task.isCompletedNormally() : 任务完成时没有出错
  2. task.isCompletedAbnormally() : 来检查任务是否已经抛出异常或已经被取消了,要注意此方法。由于提交任务之后,检测该任务是否有异常,不是阻塞的。所以需要等待任务的完成。才能正确的获取到是否有异常
  3. task.getException() : 获得任务中抛出的异常
  4. 该类中抛出的异常,只要一抛出异常,子任务都不会再继续执行。(反正就是说只要抛出了异常,任务结果肯定是不正确的了)
  5. completeExceptionally(Throwable ex) : 该方法 可以在语义上抛出一个异常,包括非运行时异常。要在获取结果前 通过task.isCompletedAbnormally()来配合操作。

示例

场景描述:就是一个很简单的计算数组的累加值。

/**
 * Created by zhuqiang on 2015/9/14 0014.
 */
public class Client {
    public static void main(String[] args) throws InterruptedException {
        int[] arrs = {1,2,3,4,5,6,7,8,9,10};
        ForkJoinPool pool = new ForkJoinPool(1);
        Task task = new Task(arrs, 0, arrs.length);
        pool.execute(task);

        pool.shutdown(); //关闭执行器,并配合超时。来等待任务运行完成,发现一个特性:该执行器如来里面没有可活动的任务。执行器会自动关闭。而且调用get会阻塞任务直到返回结果
        pool.awaitTermination(1, TimeUnit.DAYS);

        //task.isCompletedNormally() 任务完成时没有出错
        if(task.isCompletedAbnormally()){  // 来检查任务是否已经抛出异常或已经被取消了,要注意此方法。由于提交任务之后,检测该任务是否有异常,不是阻塞的。所以需要上面的等待任务的完成。才能正确的获取到是否有异常
            System.out.println("检测到任务中有抛出的异常:" + task.getException().getMessage());
        }else{
            System.out.println(task.join());
        }
    }
}

class Task extends RecursiveTask<Integer>{
    private int[] arrs;  //要处理的数据
    private int start;  //开始索引
    private int end; //结束索引

    public Task(int[] arrs, int start, int end) {
        this.arrs = arrs;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {

        int result = 0;
        if(end - start < 2){
            for (int i = start; i < end; i++) {
                result+=arrs[i];
            }
            System.out.printf("%s,结果:%s\n", Thread.currentThread().getName(),result);
            return result;
        }else{
            int mid = (start + end) / 2;
//            System.out.println(mid);
            if(mid == 2){
                throw new RuntimeException("故意抛出的测试异常");  // 为了测试抛出异常,可以 关闭测异常。运行查看结果
//                Exception e = new Exception("故意抛出的非运行时异常");
//                completeExceptionally(e);  //也可以使用 该方法,设置一个异常,因为 源码 setExceptionalCompletion 是设置的异常,就相当于该异常并没有被抛出。在语义上通过task.isCompletedAbnormally()来抛出了非运行时异常
//                return null;  // 如果不返回,程序将继续执行后面的代码,并不能达到真正抛出异常的效果
            }

            // 拆分成2个子任务继续检测和执行
            Task task1 = new Task(arrs, start, mid);
            Task task2 = new Task(arrs, mid, end);
            invokeAll(task1, task2);  //使用同步的方式 执行
            try {
                result = task1.get() + task2.get();  // 把子任务返回的结果相加
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        return result;
    }
}

某一次的运行结果:

ForkJoinPool-1-worker-1,结果:6
ForkJoinPool-1-worker-1,结果:7
ForkJoinPool-1-worker-1,结果:8
ForkJoinPool-1-worker-1,结果:9
ForkJoinPool-1-worker-1,结果:10
检测到任务中有抛出的异常:java.lang.RuntimeException: 故意抛出的测试异常

结果说明:
  把故意抛出的异常注释掉。就能看到正确的结果,而本示例,故意抛出的异常,使用task.getException() 能获取到抛出的异常。


5.6.取消任务

  本章我们将学习怎么在 Fork/Join中取消任务。ForkJoinTask 对象中有一个cancel()方法来取消未开始的任务。取消任务有以下两点需要注意:
1. ForkJoinPool类不提供任何方法来取消线程池中正在运行或则等待运行的所有任务。
2. 取消任务时,不能取消已经被执行的任务。

有了以上两点,那么我们要取消任务就得想办法在compute方法中把创建的子任务。来取消掉(创建的任务肯定是ForkJoinTask类型的);
——- 根据我的测试:不能取消任务。取消任务。就不能获取他的返回结果了。所以下面的示例:根本就没有正确的执行到。 这个示例中,没有抛出异常,不知道是为什么,但是我自己写的 会直接抛出异常。 而出现的结果也和这里的一样,不是正确的执行完的。 所以。这个取消任务是没有任何意义的。

/**
 * Created by zhuqiang on 2015/9/14 0014.
 */
public class Client {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int[] arrs = new ArrayGenerator().generateArray(50);
        TaskManger taskManger= new TaskManger();
        ForkJoinPool pool = new ForkJoinPool();
        SearchNumberTask task = new SearchNumberTask(arrs, 0, arrs.length, 5, taskManger);

        pool.execute(task);
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MILLISECONDS);

//        System.out.println("main:结束:" + task.get());
    }
}

//数组生成
class ArrayGenerator{
    public int[] generateArray(int size){
        int[] array =  new int[size];
        Random random = new Random();
        for (int i = 0; i < size; i++) {
            array[i] = random.nextInt(10);
        }
        return array;
    }
}

//任务管理类
class TaskManger{
    private List<ForkJoinTask<Integer>> tasks = new ArrayList<ForkJoinTask<Integer>>();
    public void addTask(ForkJoinTask<Integer> task){
        tasks.add(task);
    }
    public void cancelTasks(ForkJoinTask<Integer> cancelTask){
        for (ForkJoinTask<Integer> task : tasks) {
            if(task != cancelTask){
                task.cancel(true);
                ((SearchNumberTask)task).writeCanceMesg();
            }
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class SearchNumberTask extends RecursiveTask<Integer>{
    private int[] numbers;
    private int start,end;
    private int number;
    private TaskManger taskManger;

    private final static int NOT_FOUND = -1;
    public SearchNumberTask(int[] numbers, int start, int end, int number, TaskManger taskManger) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
        this.number = number;
        this.taskManger = taskManger;
    }

    @Override
    protected Integer compute() {

        int ret;
        if(end - start > 10){  //拆分任务

            ret = launchTasks();

        }else{ //执行查找
            System.out.println("Task:开始:" + start + ":" + end);
            ret = lookForNumber();
            System.out.println("Task:结束--------:" + start + ":" + end);
        }

        return ret;
    }

    /**
     * 查找数字
     * @return
     */
    private int lookForNumber() {
        for (int i = start; i < end; i++) {
            if(numbers[i] == number){
                System.out.printf("Task:目标number:%s已被找到,索引位置:%s\n", number, i);
                taskManger.cancelTasks(this);
                return i;
            }
        }
        return NOT_FOUND;
    }

    /**
     * 拆分任务
     * @return
     */
    private int launchTasks() {
        int mid = (start + end) /2;
        SearchNumberTask task1 = new SearchNumberTask(numbers,start,mid,number,taskManger);
        SearchNumberTask task2 = new SearchNumberTask(numbers,mid,end,number,taskManger);
        taskManger.addTask(task1);
        taskManger.addTask(task2);
        task1.fork(); // 异步执行
        task2.fork();

        int result = task1.join();
        if(result != -1){
            return result;
        }
        return task2.join();
    }

    /** 取消任务 信息**/
    public void writeCanceMesg(){
        System.out.printf("Task:取消了,start=%s,end=%s\n",start,end);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值