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

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
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值