【java并发工具类-分工】Fork/Join:单机版的MapReduce

分治

  • 1.站在任务分工的并发场景及解决方案
  • 2."分治"任务模型
  • 3.Fork/Join的使用
  • 3.Fork/Join实现斐波那契数列
  • 4.ForkJoinPool工作原理
  • 4.模拟 MapReduce 统计单词数量
  • 5.注意

1.站在任务分工的并发场景及解决方案

前面我们站在任务分工的角度来介绍java工具类,今天来介绍一下使用场景:

  • 对于简单的并行任务,我们可以使用简单的"线程池+Future"方案来解决;
  • 如果任务之间有OR,AND聚合关系,都可以通过CompletableFuture来解决;
  • 而对于批量执行的任务(关注点在获得任务先执行结束的Future),可以通过CompletionService来解决。
    上面的简单并行、聚合、批量执行三种任务模型,基本覆盖日常生活中的并发场景,但是还有一种分治任务模型没有说明。

2."分治"任务模型

分治:顾名思义就是分而治之,把一个问题分解成多个子问题,把子问题分解成更小的子问题,知道可以简单直接求解。算法领域中,归并排序,快速排序,二分查找等都有涉及,大数据知名框架MapReduce背后的思想也是分治。

分治任务模型:
分两个阶段:一个是任务分解;另一个是结果合并。
【java并发工具类-分工】Fork/Join:单机版的MapReduce_第1张图片
当然,java也支持这种思想,java并发包中提供一种Fork/Join的并行计算框架,就是支持分治任务模型的。

3.Fork/Join的使用

Fork/Join是一个并行计算框架,主要用来支持分治任务模型的,这个计算框架中Fork对应的分治任务模型中的任务分解,join对应的是任务合并。

Fork/Join计算框架主要包括两部分,一部分是分治任务的线程池ForkJoinPool,另一部分是分治任务ForkJoinTask。有点类似于线程池ThreadPoolExecutor和Runable的关系。

ForkJoinTask是一个抽象类,它的方法很多,最核心的方法是fork()方法和join()方法。fork():会异步执行一个子任务,而join():会阻塞当前线程等待子任务执行结果。

ForkJoinTask有两个子类——RecursiveAction和RecursiveTask,看名字应该能看出来都是通过递归处理分治任务的。这两个子类都定义了抽象方法compute(),不过区别是RecursiveAction定义的compute没有返回值,而RecursiveTask定义的compute有返回值,同样这两个子类也是抽象方法,需要子类继承使用。

3.Fork/Join实现斐波那契数列

static void main(String[] args){
  //创建分治任务线程池  
  ForkJoinPool fjp =  new ForkJoinPool(4);
  //创建分治任务
  Fibonacci fib =   new Fibonacci(30);   
  //启动分治任务  
  Integer result =   fjp.invoke(fib);
  //输出结果  
  System.out.println(result);
}
//递归任务
static class Fibonacci extends    RecursiveTask<Integer>{
  final int n;
  Fibonacci(int n){this.n = n;}
  protected 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();
  }
}

4.ForkJoinPool工作原理

前面我们知道线程池ThreadPoolExecutor本质上是生产者消费者模式,内部只有一个任务队列,消费者和生产者通过任务队列通信,线程池可以有多个工作线程,但是这些工作线程都共享同一个任务队列

ForkJoinPool本质上也是生产者消费者模式,但是更加智能,可以参考下面的图来理解ForkJoinPool的工作原理。

  1. 可以看出ForkJoinPool内部有多个任务队列,每个任务队列都有线程去消费任务。
  2. 当我们通过ForkJoinPool的invoke或者submit()方法提交任务时,ForkJoinPool根据一定的路由规则把任务提交到其中一个任务队列中。
  3. 如果任务在执行过程中创建出子任务,那么子任务会提交到工作线程对应的任务队列中。
  4. 除此之外,ForkJoinPool还支持“任务窃取”机制:就是当某个线程对应的任务队列为空时,那么它可以窃取其他任务队列中的任务,如此一来所有的工作线程都不会空闲了。
  5. ForkJoinPool中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别从任务队列的不同端获取,避免很多不必要的数据纷争。
    【java并发工具类-分工】Fork/Join:单机版的MapReduce_第2张图片

4.模拟 MapReduce 统计单词数量

学习 MapReduce 有一个入门程序,统计一个文件里面每个单词的数量,下面我们来看看如何用 Fork/Join 并行计算框架来实现。

我们可以先用二分法递归地将一个文件拆分成更小的文件,直到文件里只有一行数据,然后统计这一行数据里单词的数量,最后再逐级汇总结果。

下面的示例程序用一个字符串数组 String[] fc 来模拟文件内容,fc 里面的元素与文件里面的行数据一一对应。
关键的代码在 compute() 这个方法里面,这是一个递归方法,前半部分数据 fork 一个递归任务去处理(关键代码 mr1.fork()),后半部分数据则在当前任务中递归处理(mr2.compute())。

static void main(String[] args){
  String[] fc = {
   "hello world",
   "hello me", 
   "hello fork" ,
   "hello join", 
   "fork join in world"};
  //创建ForkJoin线程池    
  ForkJoinPool fjp =   new ForkJoinPool(3);
  //创建任务    
  MR mr = new MR(   fc, 0, fc.length);  
  //启动任务    
  Map<String, Long> result =   fjp.invoke(mr);
  //输出结果    
  result.forEach((k, v)->  System.out.println(k+":"+v));
}
//MR模拟类
static class MR extends   RecursiveTask<Map<String, Long>> {
  private String[] fc;
  private int start, end;
  //构造函数
  MR(String[] fc, int fr, int to){
    this.fc = fc;
    this.start = fr;
    this.end = to;
  }
  @Override protected 
  Map<String, Long> compute(){
    if (end - start == 1) {
      return calc(fc[start]);
    } else {
      int mid = (start+end)/2;
      MR mr1 = new MR( fc, start, mid);
      mr1.fork();
      MR mr2 = new MR( fc, mid, end);
      //计算子任务,并返回合并的结果    
      return merge(mr2.compute(),   mr1.join());
    }
  }
  //合并结果
  private Map<String, Long> merge(  Map<String, Long> r1,   Map<String, Long> r2) {
    Map<String, Long> result =   new HashMap<>();
    result.putAll(r1);
    //合并结果
    r2.forEach((k, v) -> {
      Long c = result.get(k);
      if (c != null)
        result.put(k, c+v);
      else 
        result.put(k, v);
    });
    return result;
  }
  //统计单词数量
  private Map<String, Long>  calc(String line) {
    Map<String, Long> result = new HashMap<>();
    //分割单词    
    String [] words =  line.split("\\s+");
    //统计单词数量    
    for (String w : words) {
      Long v = result.get(w);
      if (v != null) 
        result.put(w, v+1);
      else
        result.put(w, 1L);
    }
    return result;
  }
}

5.注意

ForkJoin并行计算框架的核心组件是ForkJoinPool,ForkJoinPool支持任务窃取机制,这样能够让所有线程的任务工作量均等,不会出现有的线程忙,有的线程空闲。性能很好。

Java 1.8 提供的 Stream API 里面并行流也是以 ForkJoinPool 为基础的。不过,默认情况下所有的并行流计算都共享一个 ForkJoinPool,**这个共享的 ForkJoinPool 默认的线程数是 CPU 的核数;**如果所有的并行流计算都是 CPU 密集型计算的话,完全没有问题,但是如果存在 I/O 密集型的并行流计算,那么很可能会因为一个很慢的 I/O 计算而拖慢整个系统的性能。所以建议用不同的 ForkJoinPool 执行不同类型的计算任务。

你可能感兴趣的:(并发编程体系架构,#,java并发工具类)