Java并发编程五:Fork/Join框架介绍

来源:互联网 发布:c语言编程软件dev 编辑:程序博客网 时间:2024/05/16 01:35

1、Fork/Join框架是什么

Fork/Join框架是一个比较特殊的线程池框架,专用于需要将一个任务不断分解成多个子任务(分支),并将多个子任务的结果不断进行汇总得到最终结果(聚合)的并行计算框架。

这里写图片描述
Fork/Join框架示例图(图片来自互联网)

2、相关类说明

ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:

  • RecursiveTask:用于有返回结果的任务。
  • RecursiveAction:用于没有返回结果的任务。

继承关系如下图所示:
这里写图片描述

ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。比起传统的线程池类ThreadPoolExecutor,ForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率。

ForkJoinPool 提供了三类方法来调度子任务:

  • execute 系列:异步执行指定的任务。
  • invoke 和 invokeAll:执行指定的任务,等待完成,返回结果。
  • submit 系列:异步执行指定的任务并立即返回一个 Future 对象。

继承关系如下图所示:
这里写图片描述

3、使用Fork/Join框架

任务说明:采用Fork/Join框架遍历指定目录(含子目录)中的文件数量。

任务实现逻辑:
1、获取指定目录中的文件和子目录
2、遍历目录中的文件和子目录
3、如果是文件则计数器加1
4、如果是目录,则递归创建子任务
5、将各任务的结果进行汇总形成最终结果

import java.io.IOException;import java.nio.file.DirectoryStream;import java.nio.file.Files;import java.nio.file.LinkOption;import java.nio.file.Path;import java.nio.file.Paths;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ExecutionException;import java.util.concurrent.ForkJoinPool;import java.util.concurrent.Future;import java.util.concurrent.RecursiveTask;/** * <p>Title: CountingTask</p> * <p>Description: 统计指定目录(含子目录)的文件数量</p> * @author  liuzhibo * @date    2016年8月17日 */public class CountingTask extends RecursiveTask<Integer> {    private Path dir;    public CountingTask(Path dir) {        this.dir = dir;    }    @Override    protected Integer compute() {        int count = 0;        List<CountingTask> subTasks = new ArrayList<>();        // Opens a directory, returning a DirectoryStream to iterate over all entries in the directory.         // 打开目录,返回一个DirectoryStream对象,用来遍历这个目录下的所有实体。        try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {            for (Path subPath : ds) {                if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) {                    // 对每个子目录都新建一个子任务。                    subTasks.add(new CountingTask(subPath));                } else {                    // 遇到文件,则计数器增加 1。                    count++;                }            }            if (!subTasks.isEmpty()) {                // 在当前的 ForkJoinPool 上调度所有的子任务。                for (CountingTask subTask : invokeAll(subTasks)) {                    count += subTask.join();                }            }        } catch (IOException ex) {            return 0;        }        return count;    }    public static void main(String [] args){        try {            // 用一个 ForkJoinPool 实例调度“总任务”            System.out.println("task start");            ForkJoinPool pool = new ForkJoinPool();            CountingTask task = new CountingTask(Paths.get("D:/"));            Integer count = 0;            Future<Integer> result = pool.submit(task);            count = result.get();            System.out.println("文件数量:" + count);            System.out.println("task end");        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}

分析:
1、DirectoryStream<Path> ds = Files.newDirectoryStream(dir)获取指定目录中的文件和子目录
2、for (Path subPath : ds) {}遍历目录中的文件和子目录
3、count++;如果是文件则计数器加1
4、if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)){subTasks.add(new CountingTask(subPath));}如果是目录,则递归创建子任务
5、count += subTask.join();将各任务的结果进行汇总形成最终结果

4、Fork/Join框架的异常处理

ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。使用如下代码:

if(task.isCompletedAbnormally()){    System.out.println(task.getException());}

getException方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null。

5、应用场景

Fork/Join框架适合能够进行拆分再合并的计算密集型(CPU密集型)任务。Fork/Join框架是一个并行框架,因此要求服务器拥有多CPU、多核,用以提高计算能力。

如果是单核、单CPU,不建议使用该框架,会带来额外的性能开销,反而比单线程的执行效率低。当然不是因为并行的任务会进行频繁的线程切换,因为Fork/Join框架在进行线程池初始化的时候默认线程数量为Runtime.getRuntime().availableProcessors(),单CPU单核的情况下只会产生一个线程,并不会造成线程切换,而是会增加Fork/Join框架的一些队列、池化的开销。

6、约束条件

  1. 除了fork() 和 join()方法外,线程不得使用其他的同步工具。线程最好也不要sleep()
  2. 线程不得进行I/O操作
  3. 线程不得抛出checked exception
1 0