Skip to content

Latest commit

 

History

History
1445 lines (1031 loc) · 56.4 KB

File metadata and controls

1445 lines (1031 loc) · 56.4 KB

五、Fork/Join 框架

在本章中,我们将介绍:

  • 创建 Fork/Join 池
  • 加入任务的结果
  • 异步运行任务
  • 在任务中抛出异常
  • 取消任务

导言

通常,当您实现一个简单的并发 Java 应用时,您会实现一些Runnable对象,然后是相应的Thread对象。您可以控制程序中这些线程的创建、执行和状态。Java5 对ExecutorExecutorService接口以及实现它们的类(例如ThreadPoolExecutor类)进行了改进。

Executor 框架将任务创建和执行分开。使用它,您只需实现Runnable对象并使用Executor对象。您将Runnable任务发送给执行器,执行器将创建、管理和完成执行这些任务所需的线程。

Java7 更进一步,包括面向特定问题的ExecutorService接口的附加实现。它是Fork/Join 框架

该框架旨在解决可以使用分治技术分解为更小任务的问题。在任务内部,检查要解决的问题的大小,如果问题的大小大于已确定的大小,则将其划分为使用框架执行的较小任务。如果问题的大小小于已确定的大小,则直接在任务中解决问题,然后(可选)返回结果。下图总结了这一概念:

Introduction

没有公式可以确定问题的参考大小,该公式可以根据任务的特征确定任务是否细分。您可以使用任务中要处理的元素数量和执行时间估计来确定引用大小。测试不同的参考尺寸,以选择最适合您问题的尺寸。你可以把它看作是一种特殊的形式。

该框架基于以下两个操作:

  • fork操作:将任务划分为更小的任务,并使用框架执行它们
  • 加入操作:当任务等待其创建的任务完成时

Fork/Join 与 Executor 框架的主要区别在于工作窃取算法。与 Executor 框架不同,当任务等待使用联接操作创建的子任务完成时,执行该任务的线程(称为工作线程查找尚未执行的其他任务并开始执行。通过这种方式,线程可以充分利用其运行时间,从而提高应用的性能。

为了实现这一目标,Fork/Join 框架执行的任务有以下限制:

  • 任务只能使用fork()join()操作作为同步机制。如果使用其他同步机制,工作线程在同步操作中将无法执行其他任务。例如,如果您在 Fork/Join 框架中将一个任务置于睡眠状态,则执行该任务的工作线程在睡眠期间不会执行另一个任务。
  • 任务不应执行 I/O 操作,例如读取或写入文件中的数据。
  • 任务不能抛出已检查的异常。它必须包含处理它们所需的代码。

Fork/Join 框架的核心由以下两个类组成:

  • ForkJoinPool:实现ExecutorService接口和工作窃取算法。它管理工作线程,并提供有关任务状态及其执行的信息。
  • ForkJoinTask:是将在ForkJoinPool中执行的任务的基类。它提供了在任务内部执行fork()join()操作的机制以及控制任务状态的方法。通常,为了实现 Fork/Join 任务,您将实现此类中两个子类的子类:RecursiveAction用于没有返回结果的任务,RecursiveTask用于返回结果的任务。

本章介绍了五种方法,它们向您展示了如何有效地使用 Fork/Join 框架。

创建 Fork/Join 池

在本食谱中,您将学习如何使用 Fork/Join 框架的基本元素。这包括:

  • 创建一个ForkJoinPool对象来执行任务
  • 创建要在池中执行的ForkJoinTask子类

您将在本例中使用的 Fork/Join 框架的主要特征如下:

  • 您将使用默认构造函数创建ForkJoinPool

  • 在任务内部,您将使用 Java API 文档推荐的结构:

    If (problem size > default size){
      tasks=divide(task);
      execute(tasks);
    } else {
      resolve problem using another algorithm;
    }
  • 您将以同步方式执行任务。当一个任务执行两个或多个子任务时,它会等待它们的完成。通过这种方式,执行该任务的线程(称为工作线程)将寻找其他要执行的任务,充分利用它们的执行时间。

  • 您将要实现的任务不会返回任何结果,因此您将使用RecursiveAction类作为其实现的基类。

准备好了吗

此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。

怎么做。。。

在此配方中,您将执行一项任务来更新产品列表的价格。初始任务将负责更新列表中的所有元素。您将使用尺寸 10 作为参考尺寸,因此,如果任务必须更新 10 个以上的元素,它会将分配给它的列表部分分为两部分,并创建两个任务来更新相应部分中产品的价格。

按照以下步骤来实现该示例:

  1. 创建一个名为Product的类,该类将存储产品的名称和价格。

    public class Product {
  2. 声明名为name的私有String属性和名为price的私有double属性。

      private String name;
      private double price;
  3. 实现这两种方法并建立两个属性的值。

      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public double getPrice() {
        return price;
      }
    
      public void setPrice(double price) {
        this.price = price;
      }
  4. 创建一个名为ProductListGenerator的类来生成随机产品列表。

    public class ProductListGenerator {
  5. 执行generate()方法。它接收一个带有列表大小的int参数,并返回一个带有生成产品列表的List<Product>对象。

      public List<Product> generate (int size) {
  6. 创建对象以返回产品列表。

        List<Product> ret=new ArrayList<Product>();
  7. 生成产品列表。将相同的价格分配给所有产品,例如 10,以检查程序是否运行良好。

        for (int i=0; i<size; i++){
          Product product=new Product();
          product.setName("Product "+i);
          product.setPrice(10);
          ret.add(product);
        }
        return ret;
      }
  8. 创建一个名为Task的类。指定它扩展RecursiveAction类。

    public class Task extends RecursiveAction {
  9. 声明类的串行版本 UID。这个元素是必需的,因为RecursiveAction类的父类ForkJoinTask类实现了Serializable接口。

      private static final long serialVersionUID = 1L;
  10. 声明名为products的私有List<Product>属性。

```java
  private List<Product> products;
```
  1. 声明两个私有的int属性,分别命名为firstlast。这些属性将决定此任务必须处理的产品块。
```java
  private int first;
  private int last;
```
  1. 声明一个名为increment的私有double属性来存储产品价格的增量。
```java
  private double increment;
```
  1. 实现类的构造函数,该构造函数将初始化该类的所有属性。
```java
  public Task (List<Product> products, int first, int last, double increment) {
    this.products=products;
    this.first=first;
    this.last=last;
    this.increment=increment;
  }
```
  1. 实现将实现任务逻辑的compute()方法。
```java
  @Override
  protected void compute() {
```
  1. 如果lastfirst属性的差异小于 10(任务必须更新少于 10 个产品的价格),则使用updatePrices()方法增加该集合或产品的价格。
```java
    if (last-first<10) {
      updatePrices();
```
  1. 如果lastfirst属性之间的差异大于或等于 10,则创建两个新的Task对象,一个用于处理前半部分产品,另一个用于处理后半部分产品,并使用invokeAll()方法在ForkJoinPool中执行。
```java
    } else {
      int middle=(last+first)/2;
      System.out.printf("Task: Pending tasks: %s\n",getQueuedTaskCount());
      Task t1=new Task(products, first,middle+1, increment);
      Task t2=new Task(products, middle+1,last, increment);
      invokeAll(t1, t2);  
    }
```
  1. 执行updatePrices()方法。此方法更新占据产品列表中firstlast属性值之间位置的产品。
```java
  private void updatePrices() {
    for (int i=first; i<last; i++){
      Product product=products.get(i);
      product.setPrice(product.getPrice()*(1+increment));
    }
  }
```
  1. 通过创建名为Main的类来实现示例的主类,并向其添加main()方法。
```java
public class Main {
  public static void main(String[] args) {
```
  1. 使用ProductListGenerator类创建 10000 个产品的列表。
```java
    ProductListGenerator generator=new ProductListGenerator();
    List<Product> products=generator.generate(10000);
```
  1. 新建Task对象,更新列表中所有产品的产品。参数first取值0,参数last取值10,000(产品列表的大小)。
```java
      Task task=new Task(products, 0, products.size(), 0.20);
```
  1. 使用不带参数的构造函数创建一个ForkJoinPool对象。
```java
    ForkJoinPool pool=new ForkJoinPool();
```
  1. 使用execute()方法在池中执行任务。
```java
    pool.execute(task);
```
  1. 实现一个代码块,每五毫秒向控制台写入池的某些参数的值,以显示池的演变信息,直到任务完成其执行。
```java
    do {
      System.out.printf("Main: Thread Count: %d\n",pool.getActiveThreadCount());
      System.out.printf("Main: Thread Steal: %d\n",pool.getStealCount());
      System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
      try {
        TimeUnit.MILLISECONDS.sleep(5);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } while (!task.isDone());
```
  1. 使用shutdown() 方法关闭池。
```java
    pool.shutdown();
```
  1. 使用isCompletedNormally()方法检查任务是否已完成且没有错误,在这种情况下,向控制台写入消息。
```java
    if (task.isCompletedNormally()){
      System.out.printf("Main: The process has completed normally.\n");
    }
```
  1. 所有产品的预期价格在增加后为 12。写下所有价差为 12 的产品的名称和价格,以检查所有产品是否正确加价。
```java
    for (int i=0; i<products.size(); i++){
      Product product=products.get(i);
      if (product.getPrice()!=12) {
        System.out.printf("Product %s: %f\n",product.getName(),product.getPrice());
      }
    }
```
  1. 编写一条消息,指示程序已完成。
```java
    System.out.println("Main: End of the program.\n");
```

它是如何工作的。。。

在本例中,创建了一个ForkJoinPool对象和在池中执行的ForkJoinTask类的子类。要创建ForkJoinPool对象,您使用了没有参数的构造函数,因此它将以默认配置执行。它创建一个线程数等于计算机处理器数的池。当ForkJoinPool对象被创建时,这些线程被创建,它们在池中等待,直到一些任务到达执行。

由于Task类不返回结果,因此它扩展了RecursiveAction类。在配方中,您使用了建议的结构来执行任务。如果任务必须更新 10 个以上的产品,它会将这些元素集划分为两个块,创建两个任务,并为每个任务分配一个块。您已经使用了Task类中的firstlast属性来了解此任务必须在产品列表中更新的位置范围。您已使用firstlast属性仅使用产品列表的一个副本,而不是为每个任务创建不同的列表。

为了执行任务创建的子任务,它调用invokeAll()方法。这是一个同步调用,任务在继续(可能完成)其执行之前等待子任务的完成。当任务正在等待其子任务时,执行它的工作线程将接受另一个正在等待执行的任务并执行它。通过这种行为,Fork/Join 框架提供了比RunnableCallable对象本身更有效的任务管理。

ForkJoinTask类的invokeAll()方法是 Executor 和 Fork/Join 框架之间的主要区别之一。在 Executor 框架中,所有任务都必须发送给 Executor,而在这种情况下,任务包括在池中执行和控制任务的方法。您在Task类中使用了invokeAll()方法,该方法扩展了RecursiveAction类,该类扩展了ForkJoinTask类。

您已向池发送了一个独特的任务,以使用execute()方法更新所有产品列表。在本例中,它是一个异步调用,主线程继续执行。

你用来检查 T0 类运行状态的方法。该类包含更多可用于此目的的方法。有关这些方法的完整列表,请参见监控 Fork/Join 池配方。

最后,与 Executor 框架一样,您应该使用shutdown()方法完成ForkJoinPool

以下屏幕截图显示了此示例执行的一部分:

How it works...

你可以看到任务完成了他们的工作和产品的价格更新。

还有更多。。。

ForkJoinPool类提供了在中执行任务的其他方法。这些方法如下:

  • execute (Runnable task):这是示例中使用的execute()方法的另一个版本。在本例中,您向ForkJoinPool类发送Runnable任务。请注意,ForkJoinPool类没有对Runnable对象使用工作窃取算法。它仅用于ForkJoinTask对象。
  • invoke(ForkJoinTask<T> task):当execute()方法对ForkJoinPool类进行异步调用时,正如您在示例中了解到的,invoke()方法对ForkJoinPool类进行同步调用。在作为参数传递的任务完成其执行之前,此调用不会返回。
  • 您也可以使用ExecutorService界面中声明的invokeAll()invokeAny()方法。这些方法接收Callable对象作为参数。ForkJoinPool类不使用Callable对象的工作窃取算法,因此最好使用执行器执行它们。

ForkJoinTask类还包括示例中使用的invokeAll()方法的其他版本。这些版本如下:

  • invokeAll(ForkJoinTask<?>... tasks):此版本的方法使用变量参数列表。您可以将任意多个ForkJoinTask对象作为参数传递给它。
  • invokeAll(Collection<T> tasks):此版本的方法接受泛型类型T对象的集合(例如,ArrayList对象、LinkedList对象或TreeSet对象)。此泛型类型T必须是ForkJoinTask类或其子类。

虽然ForkJoinPool类设计用于执行ForkJoinTask的对象,但您也可以直接执行RunnableCallable对象。您也可以使用ForkJoinTask类的adapt()方法,该方法接受Callable对象或Runnable对象并返回ForkJoinTask对象以执行该任务。

另见

  • 第 8 章测试并发应用中的监控 Fork/Join 池配方

加入任务结果

Fork/Join 框架提供了执行返回结果的任务的能力。此类任务由RecursiveTask类实现。该类扩展了ForkJoinTask类,实现了 Executor 框架提供的Future接口。

在任务内部,必须使用 Java API 文档推荐的结构:

If (problem size > size){
  tasks=Divide(task);
  execute(tasks);
  groupResults()
  return result;
} else {
  resolve problem;
  return result;
}

如果任务必须解决大于预定义大小的问题,则可以将问题划分为更多子任务,并使用 Fork/Join 框架执行这些子任务。当它们完成执行时,发起任务获得所有子任务生成的结果,对它们进行分组,并返回最终结果。最终,当池中执行的初始任务完成其执行时,您将获得其结果,这实际上是整个问题的最终结果。

在本教程中,您将学习如何使用 Fork/Join 框架开发一个在文档中查找单词的应用来解决此类问题。您将执行以下两种任务:

  • 文档任务,它将在文档的一组行中搜索一个单词
  • 一个行任务,它将在文档的一部分中搜索一个单词

所有任务都将返回单词在其处理的文档部分或行中出现的次数。

怎么做。。。

按照以下步骤来实现该示例:

  1. 创建一个名为Document的类。它将生成模拟文档的字符串矩阵。

    public class Document {
  2. 创建一个包含一些单词的字符串数组。此数组将用于生成字符串矩阵。

    private String words[]={"the","hello","goodbye","packt", "java","thread","pool","random","class","main"};
  3. 执行generateDocument()方法。它接收行数、每行字数以及示例要查找的字数作为参数。它返回一个字符串矩阵。

      public String[][] generateDocument(int numLines, int numWords, String word){
  4. 首先,创建生成文档所需的对象:String矩阵和生成随机数的Random对象。

        int counter=0;
        String document[][]=new String[numLines][numWords];
        Random random=new Random();
  5. 用字符串填充数组。将位于单词数组中随机位置的字符串存储在每个位置,并计算程序将在生成的数组中查找的单词的出现次数。可以使用此值检查程序是否正确执行其工作。

        for (int i=0; i<numLines; i++){
          for (int j=0; j<numWords; j++) {
            int index=random.nextInt(words.length);
            document[i][j]=words[index];
            if (document[i][j].equals(word)){
              counter++;
            }
          }
        }
  6. 用单词的出现次数写一条消息,并返回生成的矩阵。

        System.out.println("DocumentMock: The word appears "+ counter+" times in the document");
        return document;
  7. 创建一个名为DocumentTask的类,并指定它扩展用Integer类参数化的RecursiveTask类。此类将实现一个任务,该任务将计算单词在一组行中的出现次数。

    public class DocumentTask extends RecursiveTask<Integer> {
  8. 声明一个名为document的私有String矩阵和两个名为startend的私有int属性。还声明名为word的私有String属性。

      private String document[][];
      private int start, end;
      private String word;
  9. 实现类的构造函数以初始化其所有属性。

      public DocumentTask (String document[][], int start, int end, String word){
        this.document=document;
        this.start=start;
        this.end=end;
        this.word=word;
      }
  10. 执行compute()方法。如果endstart属性之间的差异小于 10,则任务将计算调用processLines()方法的这些位置之间的行中单词的出现次数。

```java
  @Override
  protected Integer compute() {
      int result;
    if (end-start<10){
      result=processLines(document, start, end, word);
```
  1. 否则,将两个对象中的行分组,创建两个新的DocumentTask对象来处理这两个组,并使用invokeAll()方法在池中执行。
```java
    } else {
      int mid=(start+end)/2;
      DocumentTask task1=new DocumentTask(document,start,mid,word);
      DocumentTask task2=new DocumentTask(document,mid,end,word);
      invokeAll(task1,task2);
```
  1. 然后,使用groupResults()方法将两个任务返回的值相加。最后,返回任务计算的结果。
```java
      try {
        result=groupResults(task1.get(),task2.get());
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }
    return result;
```
  1. 执行processLines()方法。它接收任务正在搜索的字符串矩阵、start属性、end属性和word属性作为参数。
```java
  private Integer processLines(String[][] document, int start, int end,String word) {
```
  1. 对于任务必须处理的每一行,创建一个LineTask对象来处理完整的行,并将它们存储在任务列表中。
```java
    List<LineTask> tasks=new ArrayList<LineTask>();  
    for (int i=start; i<end; i++){
      LineTask task=new LineTask(document[i], 0, document[i].length, word);
      tasks.add(task);
    }
```
  1. 使用invokeAll()方法执行该列表中的所有任务。
```java
    invokeAll(tasks);
```
  1. 对所有这些任务返回的值求和并返回结果。
```java
    int result=0;
    for (int i=0; i<tasks.size(); i++) {
      LineTask task=tasks.get(i);
      try {
        result=result+task.get();
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }
    return new Integer(result);
```
  1. 实施方法的groupResults()方法。它将两个数字相加并返回结果。
```java
  private Integer groupResults(Integer number1, Integer number2) {
    Integer result;
    result=number1+number2;
    return result;
  }
```
  1. 创建一个名为LineTask的类,并指定它扩展用Integer类参数化的RecursiveTask类。此类将实现计算单词在一行中出现的次数的任务。
```java
public class LineTask extends RecursiveTask<Integer>{
```
  1. 声明类的串行版本 UID。此元素是必需的,因为RecursiveTask类的父类ForkJoinTask类实现了Serializable接口。声明一个名为line的私有String数组属性和两个名为startend的私有int属性。最后,声明一个名为word的私有String属性。
```java
  private static final long serialVersionUID = 1L;
  private String line[];
  private int start, end;
  private String word;
```
  1. 实现类的构造函数以初始化其所有属性。
```java
  public LineTask(String line[], int start, int end, String word) {
    this.line=line;
    this.start=start;
    this.end=end;
    this.word=word;
  }
```
  1. 实现类的compute()方法。如果endstart属性之间的差异小于 100,则任务使用 count()方法在startend属性确定的行片段中搜索单词。
```java
  @Override
  protected Integer compute() {
    Integer result=null;
    if (end-start<100) {
      result=count(line, start, end, word);
```
  1. 否则,将行中的单词组一分为二,创建两个新的LineTask对象来处理这两个组,并使用invokeAll()方法在池中执行它们。
```java
    } else {
      int mid=(start+end)/2;
      LineTask task1=new LineTask(line, start, mid, word);
      LineTask task2=new LineTask(line, mid, end, word);
      invokeAll(task1, task2);
```
  1. 然后,使用groupResults()方法将两个任务返回的值相加。最后,返回任务计算的结果。
```java
      try {
        result=groupResults(task1.get(),task2.get());
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }
    return result;
```
  1. 执行count()方法。它接收字符串数组,其中包含任务正在搜索的完整行、star属性、end属性和word属性作为参数。
```java
  private Integer count(String[] line, int start, int end, String word) {
```
  1. 将存储在startend属性之间的位置中的单词与任务正在搜索的word属性进行比较,如果它们相等,则增加一个counter变量。
```java
    int counter;
    counter=0;
    for (int i=start; i<end; i++){
      if (line[i].equals(word)){
        counter++;
      }
    }
```
  1. 要降低示例的执行速度,请将任务置于睡眠状态 10 毫秒。
```java
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 返回该counter变量的值。
```java
    return counter;
```
  1. 执行groupResults()方法。对两个数字求和并返回结果。
```java
  private Integer groupResults(Integer number1, Integer number2) {
    Integer result;
    result=number1+number2;
    return result;
  }
```
  1. 通过使用main()方法创建名为Main的类来实现示例的主类。
```java
public class Main{
  public static void main(String[] args) {
```
  1. 使用DocumentMock类创建包含 100 行和每行 1000 字的Document
```java
    DocumentMock mock=new DocumentMock();
    String[][] document=mock.generateDocument(100, 1000, "the");
```
  1. 创建一个新的DocumentTask对象来更新整个文档的产品。参数start取值0,参数end取值100
```java
    DocumentTask task=new DocumentTask(document, 0, 100, "the");
```
  1. 使用不带参数的构造函数创建ForkJoinPool对象,并使用execute()方法在池中执行任务。
```java
    ForkJoinPool pool=new ForkJoinPool();
    pool.execute(task);
```
  1. 实现一个代码块,该代码块显示池的进度信息,每秒向控制台写入池的某些参数的值,直到任务完成执行。
```java
    do {
      System.out.printf("******************************************\n");
      System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
      System.out.printf("Main: Active Threads: %d\n",pool.getActiveThreadCount());
      System.out.printf("Main: Task Count: %d\n",pool.getQueuedTaskCount());
      System.out.printf("Main: Steal Count: %d\n",pool.getStealCount());
      System.out.printf("******************************************\n");
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } while (!task.isDone());
```
  1. 使用shutdown()方法关闭池。
```java
    pool.shutdown();
```
  1. 使用awaitTermination() 方法等待任务完成。
```java
    try {
      pool.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 在文档中写出单词出现的次数。检查此编号是否与DocumentMock类编写的编号相同。
```java
    try {
      System.out.printf("Main: The word appears %d in the document",task.get());
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
```

它是如何工作的。。。

在本例中,您实现了两个不同的任务:

  • DocumentTask类:该类的任务必须处理由startend属性确定的一组文档行。如果这组行的大小小于 10,它将为每行创建LineTask,当它们完成执行时,它将对这些任务的结果求和,并返回求和的结果。如果任务必须处理的行集合的大小为 10 或更大,它会将该集合一分为二,并创建两个DocumentTask对象来处理这些新集合。当这些任务完成执行时,任务将对其结果求和,并返回该和作为结果。
  • LineTask类:该类的任务必须处理文档一行中的一组单词。如果这组单词小于 100,任务将直接在该组单词中搜索该单词,并返回该单词的出现次数。否则,它会将单词集一分为二,并创建两个LineTask对象来处理这些单词集。当这些任务完成执行时,任务将两个任务的结果相加,并返回该总和作为结果。

Main类中,使用默认构造函数创建了一个ForkJoinPool对象,并在其中执行了一个DocumentTask类,该类必须处理一个 100 行、每行 1000 字的文档。此任务将使用其他DocumentTask对象和LineTask对象来划分问题,当所有任务完成执行后,您可以使用原始任务来获取单词在整个文档中的出现总数。由于任务返回结果,因此它们扩展了RecursiveTask类。

为了获取Task返回的结果,您使用了get()方法。此方法在RecursiveTask类实现的Future接口中声明。

执行程序时,可以比较控制台中写入的第一行和最后一行。第一行是生成文档时计算的单词出现次数,最后一行是 Fork/Join 任务计算的相同次数。

还有更多。。。

ForkJoinTask类提供了另一种方法来完成任务的执行并返回结果,即complete()方法。此方法接受RecursiveTask类参数化中使用的类型的对象,并在调用join()方法时返回该对象作为任务的结果。建议使用它为异步任务提供结果。

由于RecursiveTask类实现了Future接口,所以get()方法还有版本:

  • get(long timeout, TimeUnit unit):此版本的get()方法,如果任务结果不可用,则等待指定时间。如果指定的时间段过去了,结果还不可用,则该方法返回一个null值。TimeUnit类是具有以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

另见

  • 第 5 章Fork/Join 框架中的创建 Fork/Join 池配方
  • 第 8 章测试并发应用中的监控 Fork/Join 池配方

异步运行任务

当在ForkJoinPool中执行ForkJoinTask时,可以同步或异步执行。当您以同步方式执行时,将任务发送到池的方法不会返回,直到发送的任务完成其执行。以异步方式执行时,将任务发送给执行器的方法会立即返回,因此任务可以继续执行。

您应该意识到这两种方法之间的巨大差异。当您使用同步方法时,调用其中一个方法(例如,invokeAll()方法)的任务将被挂起,直到它发送到池中的任务完成执行为止。这允许ForkJoinPool类使用工作窃取算法将新任务分配给执行休眠任务的工作线程。相反,当您使用异步方法(例如,fork()方法)时,任务将继续执行,因此ForkJoinPool类不能使用工作窃取算法来提高应用的性能。在这种情况下,只有在调用join()get()方法等待任务完成时,ForkJoinPool类才能使用该算法。

在这个配方中,您将学习如何使用ForkJoinPoolForkJoinTask类提供的异步方法来管理任务。您将要实现一个程序,该程序将在文件夹及其子文件夹中搜索具有确定扩展名的文件。您将要实现的ForkJoinTask类将处理文件夹的内容。对于该文件夹中的每个子文件夹,它将以异步方式向ForkJoinPool类发送一个新任务。对于该文件夹中的每个文件,任务将检查文件的扩展名,并在继续时将其添加到结果列表中。

怎么做。。。

按照以下步骤实现示例:

  1. 创建一个名为FolderProcessor的类,并指定它扩展用List<String>类型参数化的RecursiveTask类。

    public class FolderProcessor extends RecursiveTask<List<String>> {
  2. 声明类的串行版本 UID。此元素是必需的,因为RecursiveTask类的父类ForkJoinTask类实现了Serializable接口。

      private static final long serialVersionUID = 1L;
  3. 声明名为path的私有String属性。此属性将存储此任务要处理的文件夹的完整路径。

      private String path;
  4. 声明名为extension的私有String属性。此属性将存储此任务要查找的文件扩展名的名称。

      private String extension;
  5. 实现类的构造函数以初始化其属性。

      public FolderProcessor (String path, String extension) {
        this.path=path;
        this.extension=extension;
      }
  6. 执行compute()方法。当您使用List<String>类型参数化RecursiveTask类时,此方法必须返回该类型的对象。

      @Override
      protected List<String> compute() {
  7. 声明一个String对象列表,用于存储文件夹中存储的文件名。

        List<String> list=new ArrayList<>();
  8. 声明一个FolderProcessor任务列表,以存储将要处理文件夹中存储的子文件夹的子任务。

        List<FolderProcessor> tasks=new ArrayList<>();
  9. 获取文件夹的内容。

        File file=new File(path);
        File content[] = file.listFiles();
  10. 对于文件夹中的每个元素,如果存在子文件夹,则创建一个新的FolderProcessor对象,并使用fork()方法异步执行。

```java
    if (content != null) {
      for (int i = 0; i < content.length; i++) {
        if (content[i].isDirectory()) {
          FolderProcessor task=new FolderProcessor(content[i].getAbsolutePath(), extension);
          task.fork();
          tasks.add(task);
```
  1. 否则,使用checkFile()方法将文件的扩展名与您要查找的扩展名进行比较,如果它们相等,则将文件的完整路径存储在前面声明的字符串列表中。
```java
        } else {
          if (checkFile(content[i].getName())){
            list.add(content[i].getAbsolutePath());
          }
        }
      }
```
  1. 如果FolderProcessor子任务列表包含 50 个以上的元素,则向控制台写入一条消息以指示这种情况。
```java
      if (tasks.size()>50) {
        System.out.printf("%s: %d tasks ran.\n",file.getAbsolutePath(),tasks.size());
      }

```
  1. 调用辅助方法addResultsFromTask(),将此任务启动的子任务返回的结果添加到文件列表中。将字符串列表和FolderProcessor子任务列表作为参数传递给它。
```java
      addResultsFromTasks(list,tasks);
```
  1. 返回字符串列表。
```java
    return list;
```
  1. 执行的addResultsFromTasks()方法。对于存储在任务列表中的每个任务,调用join()方法,等待其完成,然后返回任务结果。使用addAll()方法将该结果添加到字符串列表中。
```java
  private void addResultsFromTasks(List<String> list,
      List<FolderProcessor> tasks) {
    for (FolderProcessor item: tasks) {
      list.addAll(item.join());
    }
  }
```
  1. 执行的checkFile()方法。此方法比较作为参数传递的文件的名称是否以您要查找的扩展名结尾。如果是,则返回true值,否则返回false值。
```java
  private boolean checkFile(String name) {
     return name.endsWith(extension);
  }
```
  1. 通过使用main()方法创建名为Main的类来实现示例的主类。
```java
public class Main {
  public static void main(String[] args) {
```
  1. 使用默认构造函数创建ForkJoinPool
```java
    ForkJoinPool pool=new ForkJoinPool();
```
  1. 创建三个FolderProcessor任务。使用不同的文件夹路径初始化每个文件夹。
```java
    FolderProcessor system=new FolderProcessor("C:\\Windows", "log");
    FolderProcessor apps=new 
FolderProcessor("C:\\Program Files","log");
    FolderProcessor documents=new FolderProcessor("C:\\Documents And Settings","log");
```
  1. 使用execute()方法执行池中的三个任务。
```java
    pool.execute(system);
    pool.execute(apps);
    pool.execute(documents);
```
  1. 每秒钟向控制台写入池状态信息,直到三个任务完成执行。
```java
    do {
      System.out.printf("******************************************\n");
      System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
      System.out.printf("Main: Active Threads: %d\n",pool.getActiveThreadCount());
      System.out.printf("Main: Task Count: %d\n",pool.getQueuedTaskCount());
      System.out.printf("Main: Steal Count: %d\n",pool.getStealCount());
      System.out.printf("******************************************\n");
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } while ((!system.isDone())||(!apps.isDone())||(!documents.isDone()));
```
  1. 使用shutdown()方法关闭ForkJoinPool
```java
    pool.shutdown();
```
  1. 将每个任务生成的结果编号写入控制台。
```java
    List<String> results;

    results=system.join();
    System.out.printf("System: %d files found.\n",results.size());

    results=apps.join();
    System.out.printf("Apps: %d files found.\n",results.size());

    results=documents.join();
    System.out.printf("Documents: %d files found.\n",results.size());
```

它是如何工作的。。。

以下屏幕截图显示了此示例的部分执行:

How it works...

本例的键在FolderProcessor类中。每个任务处理一个文件夹的内容。如您所知,此内容包含以下两种元素:

  • 文件夹
  • 其它文件夹

如果任务找到一个文件夹,它将创建另一个Task对象来处理该文件夹,并使用fork()方法将其发送到池中。此方法将任务发送到池中,如果池中有空闲工作线程或可以创建新线程,则池将执行该任务。该方法立即返回,因此任务可以继续处理文件夹的内容。对于每个文件,任务都会将其扩展名与要查找的文件进行比较,如果扩展名相等,则会将文件名添加到结果列表中。

一旦任务处理完分配文件夹的所有内容,它将等待使用join()方法发送到池中的所有任务完成。在任务中调用的此方法等待其执行的完成,并返回由compute()方法返回的值。任务将其发送的所有任务的结果与自己的结果分组,并将该列表作为compute()方法的返回值返回。

ForkJoinPool类还允许以异步方式执行任务。您已使用execute()方法将三个初始任务发送到池中。在Main类中,您还使用shutdown()方法完成了池,并编写了关于池中运行的任务的状态和演化的信息。ForkJoinPool类包含更多可用于此目的的方法。请参阅监控 Fork/Join 池配方,以查看这些方法的完整列表。

还有更多。。。

在本例中,您使用了join()方法来等待任务的完成并获得其结果。您也可以使用get()方法的两个版本中的一个来实现此目的:

  • get():此版本的get()方法返回compute()方法返回的值,如果ForkJoinTask已完成执行,或等待其完成。
  • get(long timeout, TimeUnit unit):此版本的get()方法,如果任务结果不可用,则等待指定时间。如果指定的时间段过去了,结果还不可用,则该方法返回一个null值。TimeUnit类是具有以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

get()join()方法之间有两个主要区别:

  • join()方法无法中断。如果中断调用join()方法的线程,该方法将抛出InterruptedException异常。
  • 如果任务抛出任何未检查的异常,get()方法将返回ExecutionException异常,join()方法将返回RuntimeException异常。

另见

  • 第 5 章Fork/Join 框架中的创建 Fork/Join 池配方
  • 第 8 章测试并发应用中的监控 Fork/Join 池配方

在任务中抛出异常

Java 中有两种异常:

  • 检查异常:这些异常必须在方法的throws子句中指定或捕获。例如,IOExceptionClassNotFoundException
  • 未检查的异常:不必指定或捕获这些异常。例如,NumberFormatException

您不能在ForkJoinTask类的compute()方法中抛出任何选中的异常,因为该方法的实现中不包含任何抛出声明。您必须包含处理异常所需的代码。另一方面,您可以抛出(也可以由方法内部使用的任何方法或对象抛出)未经检查的异常。ForkJoinTaskForkJoinPool类的行为与您可能期望的不同。程序未完成执行,并且在控制台中看不到有关异常的任何信息。它只是被吞下,就好像没有被扔掉一样。但是,您可以使用ForkJoinTask类的一些方法来了解任务是否引发异常以及异常的类型。在本食谱中,您将学习如何获取这些信息。

准备好了吗

此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。

怎么做。。。

按照以下步骤来实现该示例:

  1. 创建一个名为Task的类。指定它实现用Integer类参数化的RecursiveTask类。

    public class Task extends RecursiveTask<Integer> {
  2. 声明一个名为array的私有int数组。它将模拟本例中要处理的数据数组。

      private int array[];
  3. 声明两个名为startend的私有int属性。这些属性将确定此任务必须处理的数组元素。

      private int start, end;
  4. 实现初始化其属性的类的构造函数。

      public Task(int array[], int start, int end){
        this.array=array;
        this.start=start;
        this.end=end;
      }
  5. 执行任务的compute()方法。当您使用Integer类参数化RecursiveTask类时,此方法必须返回一个Integer对象。首先,使用startend属性的值向控制台写入消息。

      @Override
      protected Integer compute() {
        System.out.printf("Task: Start from %d to %d\n",start,end); 
  6. 如果此任务必须处理的元素块由startend属性确定,其大小小于 10,请检查数组中第四个位置(索引号 3)的元素是否在该块中。如果是这种情况,抛出一个RuntimeException 异常。然后,让任务休眠一秒钟。

        if (end-start<10) {
          if ((3>start)&&(3<end)){
            throw new RuntimeException("This task throws an"+ "Exception: Task from  "+start+" to "+end);
          }      
          try {
            TimeUnit.SECONDS.sleep(1);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
  7. 否则(此任务必须处理的元素块大小为 10 或更大),将元素块一分为二,创建两个Task对象来处理这些块,并使用invokeAll()方法在池中执行它们。

        } else {
          int mid=(end+start)/2;
          Task task1=new Task(array,start,mid);
          Task task2=new Task(array,mid,end);
          invokeAll(task1, task2);
        }
  8. 向控制台写入一条消息,指示写入startend属性值的任务结束。

        System.out.printf("Task: End form %d to %d\n",start,end);
  9. 返回编号0作为任务的结果。

        return 0;
  10. 通过使用main()方法创建名为Main的类来实现示例的主类。

```java
public class Main {
  public static void main(String[] args) {
```
  1. 创建一个包含 100 个整数的数组。
```java
    int array[]=new int[100];
```
  1. 创建一个Task对象来处理该数组。
```java
    Task task=new Task(array,0,100);
```
  1. 使用默认构造函数创建一个ForkJoinPool对象。
```java
    ForkJoinPool pool=new ForkJoinPool();
```
  1. 使用execute()方法执行池中的任务。
```java
    pool.execute(task);
```
  1. 使用shutdown()方法关闭ForkJoinPool类。
```java
    pool.shutdown();
```
  1. 使用awaitTermination()方法等待任务完成。由于您希望等待任务的完成,无论需要多长时间才能完成,请将值1TimeUnit.DAYS 作为参数传递给此方法。
```java
    try {
      pool.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 使用isCompletedAbnormally()方法检查任务或其子任务是否引发异常。在这种情况下,向控制台写入一条消息,其中包含抛出的异常。使用ForkJoinTask类的getException()方法获取该异常。
```java
    if (task.isCompletedAbnormally()) {
      System.out.printf("Main: An exception has ocurred\n");
      System.out.printf("Main: %s\n",task.getException());
    }
    System.out.printf("Main: Result: %d",task.join());
```

它是如何工作的。。。

在此配方中实现的Task类处理一个数字数组。它检查必须处理的数字块是否有 10 个或更多元素。在这种情况下,它将块一分为二,并创建两个新的Task对象来处理这些块。否则,它将在数组的第四个位置(索引号 3)查找元素。如果该元素位于任务必须处理的块中,它将抛出一个RuntimeException异常。

当您执行程序时,会抛出异常,但程序不会停止。在Main类中,您使用原始任务调用了ForkJoinTask类的isCompletedAbnormally()方法。如果该任务或其子任务之一引发异常,则此方法返回true。您还使用同一对象的getException()方法获取它抛出的Exception对象。

当您在任务中抛出未经检查的异常时,它还会影响其父任务(将其发送到ForkJoinPool类的任务)及其父任务的父任务,依此类推。如果修改程序的所有输出,您将看到没有用于完成某些任务的输出消息。这些任务的说明信息如下:

Task: Starting form 0 to 100
Task: Starting form 0 to 50
Task: Starting form 0 to 25
Task: Starting form 0 to 12
Task: Starting form 0 to 6

这些任务是引发异常及其父任务的任务。他们都不正常地完成了。考虑到这一点,当您使用ForkJoinPoolForkJoinTask对象开发一个程序时,若您不希望出现这种行为,可以抛出异常。

以下屏幕截图显示了此示例执行的一部分:

How it works...

还有更多。。。

如果不抛出异常,而是使用ForkJoinTask类的completeExceptionally()方法,则可以获得与示例中相同的结果。代码如下所示:

Exception e=new Exception("This task throws an Exception: "+ "Task from  "+start+" to "+end);
completeExceptionally(e);

另见

  • 第 5 章Fork/Join 框架中的创建 Fork/Join 池配方

取消任务

当您执行ForkJoinPool类中的ForkJoinTask对象时,可以在它们开始执行之前取消它们。ForkJoinTask类为此提供了cancel()方法。要取消任务,您必须考虑以下几点:

  • ForkJoinPool类没有提供任何方法来取消它在池中运行或等待的所有任务
  • 取消任务时,不会取消此任务已执行的任务

在此配方中,您将实现一个取消ForkJoinTask对象的示例。您将在数组中查找数字的位置。找到编号的第一个任务将取消其余任务。由于 Fork/Join 框架不提供该功能,因此您将实现一个辅助类来执行此取消操作。

准备好了。。。

此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE NetBeans,请打开它并创建一个新的 Java 项目

怎么做。。。

按照以下步骤来实现该示例:

  1. 创建一个名为ArrayGenerator的类。此类将生成具有指定大小的随机整数数组。实现一个名为generateArray()的方法。将生成数字数组。它接收数组的大小作为参数。

    public 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;
      }
  2. 创建一个名为TaskManager的类。我们将使用这个类来存储在示例中使用的ForkJoinPool中执行的所有任务。由于ForkJoinPoolForkJoinTask类的限制,您将使用该类取消ForkJoinPool 类的所有任务。

    public class TaskManager {
  3. 声明一个用名为ListInteger类参数化的ForkJoinTask类参数化的对象列表。

      private List<ForkJoinTask<Integer>> tasks;
  4. 实现类的构造函数。它初始化任务列表。

      public TaskManager(){
        tasks=new ArrayList<>();
      }
  5. 执行addTask()方法。它向任务列表中添加一个ForkJoinTask对象。

      public void addTask(ForkJoinTask<Integer> task){
        tasks.add(task);
      }
  6. 执行cancelTasks()方法。它将使用cancel()方法取消列表中存储的所有ForkJoinTask对象。它作为参数接收要取消其余任务的ForkJoinTask对象。该方法取消所有任务。

      public void cancelTasks(ForkJoinTask<Integer> cancelTask){
        for (ForkJoinTask<Integer> task  :tasks) {
          if (task!=cancelTask) {
            task.cancel(true);
            ((SearchNumberTask)task).writeCancelMessage();
          }
        }
      }
  7. 实现的SearchNumberTask类。指定它扩展用Integer类参数化的RecursiveTask类。此类将在整数数组的元素块中查找数字。

    public class SearchNumberTask extends RecursiveTask<Integer> {
  8. 声明名为arrayint数字的私有数组。

      private int numbers[];
  9. 声明两个名为startend的私有int属性。这些属性将确定此任务必须处理的数组元素。

      private int start, end;
  10. 声明一个名为number的私有int属性来存储要查找的号码。

```java
  private int number;
```
  1. 声明名为manager的私有TaskManager属性。您将使用此对象取消所有任务。
```java
  private TaskManager manager;
```
  1. 声明一个私有int常量并将其初始化为-1值。它将是任务在找不到数字时返回的值。
```java
  private final static int NOT_FOUND=-1;
```
  1. 实现类的构造函数以初始化其属性。
```java
  public Task(int numbers[], int start, int end, int number, TaskManager manager){
    this.numbers=numbers;
    this.start=start;
    this.end=end;
    this.number=number;
    this.manager=manager;
  }
```
  1. 执行compute()方法。通过向控制台写入指示startend属性值的消息来启动该方法。
```java
  @Override
  protected Integer compute() {
    System.out.println("Task: "+start+":"+end);
```
  1. 如果startend属性之间的差异大于 10(任务必须处理数组中的 10 个以上元素),则调用launchTasks()方法将该任务的工作划分为两个子任务。
```java
    int ret;
    if (end-start>10) {
      ret=launchTasks();
```
  1. 否则,在调用lookForNumber()方法的任务必须处理的数组块中查找编号。
```java
    } else {
      ret=lookForNumber();
    }
```
  1. 返回任务的结果。
```java
    return ret;
```
  1. 执行lookForNumber()方法。
```java
  private int lookForNumber() {
```
  1. 对于此任务必须处理的元素块中的所有元素,将该元素中存储的值与您要查找的数字进行比较。如果它们相等,则向控制台写入一条消息,指示在这种情况下,使用TaskManager对象的cancelTasks()方法取消所有任务,并返回元素的位置,即找到编号的位置。
```java
    for (int i=start; i<end; i++){
      if (array[i]==number) {
        System.out.printf("Task: Number %d found in position %d\n",number,i);
        manager.cancelTasks(this);
        return i;
      }
```
  1. 在循环内部,将任务休眠一秒钟。
```java
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
```
  1. 最后,返回-1值。
```java
    return NOT_FOUND;
  }
```
  1. 执行的launchTasks()方法。首先,将此任务必须处理的数字块分成两部分,然后创建两个Task对象来处理它们。
```java
  private int launchTasks() {
    int mid=(start+end)/2;

    Task task1=new Task(array,start,mid,number,manager);
    Task task2=new Task(array,mid,end,number,manager);
```
  1. 将任务添加到TaskManager对象。
```java
    manager.addTask(task1);
    manager.addTask(task2);
```
  1. 使用fork()方法异步执行两个任务。
```java
    task1.fork();
    task2.fork();
```
  1. 等待任务的完成,如果第一个任务的结果不同,则返回给-1,或第二个任务的结果。
```java
    int returnValue;

    returnValue=task1.join();
    if (returnValue!=-1) {
      return returnValue;
    }

    returnValue=task2.join();
    return returnValue;
```
  1. 执行writeCancelMessage()方法在任务取消时写消息。
```java
  public void writeCancelMessage(){
    System.out.printf("Task: Canceled task from %d to %d",start,end);
  }
```
  1. 通过使用main()方法创建名为Main的类来实现示例的主类。
```java
public class Main {
  public static void main(String[] args) {
```
  1. 使用ArrayGenerator类创建一个包含 1000 个数字的数组。
```java
    ArrayGenerator generator=new ArrayGenerator();
    int array[]=generator.generateArray(1000);
```
  1. 创建一个TaskManager对象。
```java
    TaskManager manager=new TaskManager();
```
  1. 使用默认构造函数创建一个ForkJoinPool对象。
```java
    ForkJoinPool pool=new ForkJoinPool();
```
  1. 创建一个Task对象来处理之前生成的数组。
```java
    Task task=new Task(array,0,1000,5,manager);
```
  1. 使用execute()方法异步执行池中的任务。
```java
    pool.execute(task);
```
  1. 使用shutdown()方法关闭池。
```java
    pool.shutdown();
```
  1. 使用ForkJoinPool类的awaitTermination()方法等待任务完成。
```java
    try {
      pool.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 向控制台写入一条消息,指示程序结束。
```java
    System.out.printf("Main: The program has finished\n");
```

它是如何工作的。。。

ForkJoinTask类提供了cancel()方法,允许您在任务尚未执行时取消任务。这是非常重要的一点。如果任务已开始执行,则对cancel()方法的调用无效。该方法接收一个参数作为名为mayInterruptIfRunningBoolean值。这个名称可能会让您认为,如果您将true值传递给该方法,即使该任务正在运行,它也会被取消。Java API 文档指定,在ForkJoinTask类的默认实现中,此属性无效。任务只有在尚未开始执行时才会取消。任务的取消对该任务发送到池的任务没有影响。他们继续执行死刑。

Fork/Join 框架的一个限制是它不允许取消ForkJoinPool中的所有任务。为了克服这个限制,您已经实现了TaskManager类。它存储已发送到池的所有任务。它有一个方法可以取消它存储的所有任务。如果某个任务由于正在运行或已完成而无法取消,cancel()方法返回false值,因此您可以尝试取消所有任务,而不必担心可能的附带影响。

在本例中,您实现了一个任务,该任务在一个数字数组中查找一个数字。按照 Fork/Join 框架的建议,可以将问题划分为更小的子问题。您只对该数字的一次出现感兴趣,因此,当您找到它时,您将取消其他任务。

以下屏幕截图显示了此示例执行的一部分:

How it works...

另见

  • 第 5 章Fork/Join 框架中的创建 Fork/Join 池配方