第五章 Fork/Join框架

Java 7 并发编程实战手册目录


代码下载(https://github.com/Wang-Jun-Chao/java-concurrency)


第五章 Fork/Join框架


5.1简介

  通常,使用Java来开发一个简单的并发应用程序时,会创建一些Runnable对象,然后创建对应的Thread对象来控制程序中这些线程的创建、执行以及线程的状态。自从Java 5开始引入了 Executor和ExecutorService接口以及实现这两个接口的类(比如ThreadPoolExecutor)之后,使得Java在并发支持上得到了进一步的提升。

  执行器框架(Executor Framework)将任务的创建和执行进行了分离,通过这个框架, 只需要实现Runnable接口的对象和使用Executor对象,然后将Runnable对象发送给执行器。执行器再负责运行这些任务所需要的线程,包括线程的创建,线程的管理以及线程的结束。

  Java7则又更进了一步,它包括了 ExecutorService接口的另一种实现,用来解决特殊类型的问题,它就是Fork/Join框架,有时也称分解/合并框架。

  Fork/Join框架是用来解决能够通过分治技术(Divide and Conquer Technique)将问题拆分成小任务的问题。在一个任务中,先检查将要解决的问题的大小,如果大于一个设定的大小,那就将问题拆分成可以通过框架来执行的小任务。如果问题的大小比设定的 大小要小,就可以直接在任务里解决这个问题,然后,根据需要返回任务的结果。下面的图形总结了这个原理。

这里写图片描述

图5.1-1 任务分解图

  没有固定的公式来决定问题的参考大小(Reference Size),从而决定一个任务是需要进行拆分或不需要拆分,拆分与否仍是依赖于任务本身的特性。可以使用在任务中将要处理的元素的数目和任务执行所需要的时间来决定参考大小。测试不同的参考大小来决定解决问题最好的一个方案,将ForkJoinPool类看作一个特殊的Executor执行器类型。这个框架基于以下两种操作。

  ♦分解(Fork)操作:当需要将一个任务拆分成更小的多个任务时,在框架中执行 这些任务;

  ♦合并(Join)操作:当一个主任务等待其创建的多个子任务的完成执行。

  Fork/Join框架和执行器框架(Executor Framework)主要的区别在于工作窃取算法(Work-Stealing Algorithm)。与执行器框架不同,使用Join操作让一个主任务等待它所创建的子任务的完成,执行这个任务的线程称之为工作者线程(WorkerThread)。工作者线程寻找其他仍未被执行的任务,然后开始执行。通过这种方式,这些线程在运行时拥有所有的优点,进而提升应用程序的性能。
为了达到这个目标,通过Fork/Join框架执行的任务有以下限制。

  ♦任务只能使用fork()和join()操作当作同步机制。如果使用其他的同步机制,工作者线程就不能执行其他任务,当然这些任务是在同步操作里时。比如,如果在Fork/Join 框架中将一个任务体眠,正在执行这个任务的工作者线程在休眠期内不能执行另一个任务。

  ♦任务不能执行I/O操作,比如文件数据的读取与写入。

  ♦任务不能抛出非运行时异常(Checked Exception),必须在代码中处理掉这些异常.
Fork/Join框架的核心是由下列两个类组成的。

  ♦ForkJoinPool:这个类实现了ExecutorService 接口和工作窃取算法(Work-Stealing Algorithm)。它管理工作者线程,并提供任务的状态信息,以及任务的执行信息。

  ♦ForkJoinTask:这个类是一个将在ForkJoinPool中执行的任务的基类。

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

  ♦RecursiveAction:用于任务没有返回结果的场景。

  ♦RecursiveAction:用于任务没有返回结果的场景。

5.2创建 Fork/Join线程池

  在本节,我们将学习如何使用Fork/Join框架的基本元素。它包括:

  ♦创建用来执行任务的ForkJoinPool对象;

  ♦创建即将在线程池中被执行的任务ForkJoinTask子类。

  本范例中即将使用的Fork / Join框架的主要特性如下:

  ♦采用默认的构造器创建ForkJoinPool对象;

  ♦在任务中使用Java API文档推荐的结构。

if(problem size > default size) {
    tasks = divide(tasks);
    execute(tasks);
} else {
    resovle problem using anothor algorithm;
}

  ♦我们将以同步的方式执行任务。当一个主任务执行两个或更多的子任务时,这个主任务将等待子任务的完成。用这种方法,执行主任务的线程,称之为工作者线程(Worker Thread),它将寻找其他的子任务来执行,并在子任务执行的时间里利用所有的线程优势。

  ♦如果将要实现的任务没有返回任何结果,那么,采用RecursiveAction类作为实现任务的基类。

package com.concurrency.utils;

/**
 * 产品类,保存产品的名称和价格
 */
public class Product {
    /**
     * 名称
     */
    private String name;
    /**
     * 价格
     */
    private double price;

    /**
     * 获取产品名称
     *
     * @return 产品名称
     */
    public String getName() {
        return name;
    }

    /**
     * 设置产名品名称
     *
     * @param name 产名名称
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取产品价格
     *
     * @return 产品价格
     */
    public double getPrice() {
        return price;
    }

    /**
     * 设置产品价格
     *
     * @param price 产品价格
     */
    public void setPrice(double price) {
        this.price = price;
    }
}
package com.concurrency.utils;

import java.util.ArrayList;
import java.util.List;

/**
 * 产品生成器类,根据指定的数量创建产品
 */
public class ProductListGenerator {

    /**
     * 产品生产类
     *
     * @param size 产品数量
     * @return 产品集合
     */
    public List<Product> generate(int size) {
        List<Product> ret = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            Product product = new Product();
            product.setName("Product " + i);
            product.setPrice(10);
            ret.add(product);
        }

        return ret;
    }
}
package com.concurrency.task;

import com.concurrency.utils.Product;

import java.util.List;
import java.util.concurrent.RecursiveAction;

/**
 *任务执行类,如果产品多于10个就让分出子任务进行处理
 */
public class Task extends RecursiveAction {
    private static final long serialVersionUID = 6876633274768462482L;

    /**
     * 产品集合对象
     */
    private List<Product> products;
    /**
     * 处理的第一个产品位置
     */
    private int first;
    /**
     * 处理的最后一个产品位置
     */
    private int last;
    /**
     * 价格增长率
     */
    private double increment;

    /**
     * 构造函数,初始化属性
     *
     * @param products  产品集合对象
     * @param first     处理的第一个产品位置
     * @param last      处理的最后一个产品位置(不包含)
     * @param increment 价格增长率
     */
    public Task(List<Product> products, int first, int last, double increment) {
        this.products = products;
        this.first = first;
        this.last = last;
        this.increment = increment;
    }

    /**
     * 对产品进行计算
     */
    @Override
    protected void compute() {
        if (last - first < 10) {  // 处理的产品数少于10个就提价
            updatePrices();
        } else {  // 否则就让两个子线程去执行
            int middle = (first + last) / 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);
        }
    }

    /**
     * 更价格,将指定的范围内的产品提价
     */
    private void updatePrices() {
        for (int i = first; i < last; i++) {
            Product product = products.get(i);
            product.setPrice(product.getPrice() * (1 + increment));  // 按increment比率提价
        }
    }
}
package com.concurrency.core;

import com.concurrency.task.Task;
import com.concurrency.utils.Product;
import com.concurrency.utils.ProductListGenerator;

import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建产品生成器对象,并且门生产10000个产品
        ProductListGenerator generator = new ProductListGenerator();
        List<Product> products = generator.generate(10000);

        // 创建一个任务对象
        Task task = new Task(products, 0, products.size(), 0.2);

        // 创建一个分合池
        ForkJoinPool pool = new ForkJoinPool();

        // 执行任务
        pool.execute(task);

        // 输出分合池的信息
        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: Paralelism: %d\n", pool.getParallelism());
            try {
                TimeUnit.MILLISECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (!task.isDone());

        // 关闭分合池
        pool.shutdown();

        // 检查任务是否正常完成
        if (task.isCompletedNormally()) {
            System.out.printf("Main: The process has completed normally.\n");
        }

        // 输出价格不是12的产品
        for (Product product : products) {
            if (product.getPrice() != 12) {
                System.out.printf("Product %s: %f\n", product.getName(), product.getPrice());
            }
        }

        // 结束程序
        System.out.println("Main: End of the program.\n");
    }
}

这里写图片描述

图5.2-1 部分运行结果

  在这个范例中,我们创建了ForkJoinPool对象,和一个将在线程池中执行的ForkJoinTask的子类。使用了无参的类构造器创建了ForkJoinPool对象,因此它将执行默认的配置。创建一个线程数等于计算机CPU数目的线程池,创建好ForkJoinPool对象之后,那些线程也创建就绪了,在线程池中等待任务的到达,然后开始执行。

  由于Task类继承了 RecursiveAction类,因此不返回结果。在本节,我们使用了推荐的结构来实现任务。如果任务需要更新大于10个产品,它将拆分这些元素为两部分,创建两个任务,并将拆分的部分相应地分配给新创建的任务。通过使用Task类的first和last 属性,来获知任务将要更新的产品列表所在的位置范围。我们已经使用first和last属性, 来操作产品列表中仅有的一份副本,而没有为每一个任务去创建不同的产品列表。

  调用invokeAll()方法来执行一个主任务所创建的多个子任务。这是一个同步调用,这个任务将等待子任务完成,然后继续执行(也可能是结束)。当一个主任务等待它的子任务时,执行这个主任务的工作者线程接收另一个等待执行的任务并开始执行。正因为有了这个行为,所以说Fork/Join框架提供了一种比Runnable和Callable对象更加高效的任务管理机制。

  ForkJoinTask 类的 invokeAll()方法是执行器框架 ExecutorFramework)和 Fork/ Join框架之间的主要差异之一。在执行器框架中,所有的任务必须发送给执行器,然而,在这个示例中,线程池包含了待执行方法的任务,任务的控制也是在线程池中进行的。我们在Task类中使用了 invokeAll()方法,Task类继承了 RecursiveAction类,而 RecursiveAction 类则继承了 ForkJoinTask 类。

  我们己经发送一个唯一的任务到线程池中,通过使用execute()方法来更新所有产品的列表。在这个示例中.它是一个同步调用,主线程一直等待调用的执行。

  我们己经使用了 ForkJoinPool类的一些方法,来检查正在运行的任务的状态和演变情况。这个类包含更多的方法,可以用于任务状态的检测。参见8.5节介绍的这些方法的完整列表。

  最后,像执行器框架一样,必须调用shutdown()方法来结束ForkJoinPool的执行。

  可以看到,任务执行结束,并且产品的价格已经更新了。

  ForkJoinPool类还提供了以下方法用于执行任务。

  ♦ execute(Runnabletask):这是本范例中使用的execute()方法的另一种版本。这个方法发送一个Runnable仟务给ForkJoinPool类。需要注意的是,使用Runnable对象时 ForkJoinPool 类就不采用工作窃取算法(Work-StealingAlgorithm),ForkJoinPool 类仅 在使用ForkJoinTask类时才采用工作窃取算法。

  ♦ invoke(ForkJoinTask task):正如范例所示,ForkJoinPool 类的 execute()方法是异步调用的,而ForkJoinPool类的invoke()方法则是同步调用的。这个方法直到传递进来的任务执行结束后才会返回。

  ♦ 也可以使用在ExecutorService类中声明的invokeAll()和invokeAny()方法,这些方法接收Callable对象作为参数。使用Callable对象时ForkJoinPool类就不采用工作窃取算法(Work-Stealing Algorithm),因此,最好使用执行器来执行Callable对象。

  ForkJoinTask类也包含了在范例中所使用的imokeAll()方法的其他版本,这些版本如下。

  ♦ invokeAll(ForkJoinTask<?> tasks):这个版本的方法接收一个可变的参数列表, 可以传递尽可能多的ForkJoinTask对象给这个方法作为参数。

  ♦ invokeAll(Collection tasks>:这个版本的方法接受一个泛型类型T的对象集合(比如,AarrayList对象、LinkedList对象或者TreeSet对象)。这个泛型类型T必须是 ForkJoinTask类或者它的子类。
虽然ForkJoinPool类是设计用来执行ForkJoinTask对象的,但也可以直接用来执行 Runnable和Callable对象。当然,也可以使用ForkJoinTask类的adapts方法来接收一个 Callable对象或者一个Runnable对象,然后将之转化为一个ForkJoinTask对象,然后再去执行。

5.3合并任务的结果

  Fork/Join框架提供了执行任务并返回结果的能力。这些类型的任务都是通过 RecursiveTask类来实现的。RecursiveTask类继承了 ForkJoinTask类,并且实现了由执行器框架(Executor Framework)提供的 Future 接口。

  在任务中,必须使用JavaAPI文档推荐的如下结构:

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

  如果任务需要解决的问题大于预先定义的大小,那么就要将这个问题拆分成多个子任务,并使用Fork/Join框架来执行这些子任务.执行完成后,原始任务获取到由所有这些子任务产生的结果,合并这些结果,返回最终的结果。当原始任务在线程池中执行结束后, 将高效地获取到整个问题的最终结果。

  在本节,我们将学习如何使用Fork/Join框架来解决这种问题,开发一个应用程序, 在文档中查找一个词。我们将实现以下两种任务:

  ♦ 一个文档任务,它将遍历文档中的每一行来查找这个词;

  ♦ 一个行任务,它将在文档的一部分当中查找这个词。

  所有这些任务将返回文档或行中所出现这个词的次数。

package com.concurrency.utils;

import java.util.Random;

/**
 * 文档模拟对象,根据指定的列数和每行单词数生成文档
 */
public class DocumentMock {
    /**
     * 文档中的单词集合
     */
    private String words[] = {"the", "hello", "goodbye", "packt", "java", "thread", "pool", "random", "class", "main"};

    /**
     * 生成文档
     *
     * @param numLines 文档行数
     * @param numWords 每行单词数
     * @param word     文档中要查找的单词
     * @return 文档
     */
    public String[][] generateDocument(int numLines, int numWords, String word) {

        int counter = 0;
        String document[][] = new String[numLines][numWords];
        Random random = new Random();
        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++;
                }
            }
        }
        System.out.printf("DocumentMock: The word appears %d times in the document.\n", counter);
        return document;
    }
}
package com.concurrency.task;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;

/**
 * 文档处理类,在文档中查找指定的单词
 */
public class DocumentTask extends RecursiveTask<Integer> {
    private static final long serialVersionUID = -1257254196502539272L;
    /**
     * 等待处理的文档
     */
    private String document[][];
    /**
     * 文档处理的开始行
     */
    private int start;
    /**
     * 文档处理的结束行
     */
    private int end;

    /**
     * 要查找的单词
     */
    private String word;

    /**
     * 构造函数
     *
     * @param document 等待处理的文档
     * @param start    文档处理的开始行
     * @param end      文档处理的结束行
     * @param word     要查找的单词
     */
    public DocumentTask(String document[][], int start, int end, String word) {
        this.document = document;
        this.start = start;
        this.end = end;
        this.word = word;
    }


    /**
     * 核心方法,统计文档的中指定的单词
     *
     * @return 指定的单词出现的次数
     */
    @Override
    protected Integer compute() {
        Integer result = null;
        if (end - start < 10) {  // 少于10行,使用行处理方法
            result = processLines(document, start, end, word);
        } 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);
            try {
                result = groupResults(task1.get(), task2.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 返回number1+number2的和
     *
     * @param number1 加数l
     * @param number2 加数2
     * @return 和
     */
    private Integer groupResults(Integer number1, Integer number2) {
        return number1 + number2;
    }

    /**
     * 行处理方法
     *
     * @param document 等待处理的文档
     * @param start    文档处理的开始行
     * @param end      文档处理的结束行
     * @param word     要查找的单词
     * @return 指定的单词出现的次数
     */
    private Integer processLines(String[][] document, int start, int end, String word) {
        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);
        }
        invokeAll(tasks);

        // 统计结果
        int result = 0;
        for (LineTask task : tasks) {
            try {
                result = result + task.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
package com.concurrency.task;

import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.TimeUnit;

/**
 * 行处理类,处理指定行数和单词
 */
public class LineTask extends RecursiveTask<Integer> {
    private static final long serialVersionUID = 4169105159737293155L;
    /**
     * 文档中一行数据
     */
    private String line[];

    /**
     * 行处理的起始位置
     */
    private int start;
    /**
     * 行处理的结束位置
     */
    private int end;

    /**
     * 要查找的单词
     */
    private String word;

    /**
     * 构造函数
     *
     * @param line  文档中一行数据
     * @param start 行处理的起始位置
     * @param end   行处理的结束位置
     * @param word  要查找的单词
     */
    public LineTask(String line[], int start, int end, String word) {
        this.line = line;
        this.start = start;
        this.end = end;
        this.word = word;
    }

    /**
     * 核心方法,完成单词的查找
     *
     * @return 查找范围内,单词出现的次数
     */
    @Override
    protected Integer compute() {
        Integer result = null;
        if (end - start < 100) { // 少于100个单词就进行统计
            result = count(line, start, end, word);
        } else { // 否则就分成两个线程进行处理
            int mid = (start + end) / 2;
            LineTask task1 = new LineTask(line, start, mid + 1, word);
            LineTask task2 = new LineTask(line, mid + 1, end, word);
            invokeAll(task1, task2);

            try {
                result = groupResults(task1.get(), task2.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 返回number1+number2的和
     *
     * @param number1 加数l
     * @param number2 加数2
     * @return 和
     */
    private Integer groupResults(Integer number1, Integer number2) {
        return number1 + number2;
    }

    /**
     * 统计单词出现的次数
     *
     * @param line  待待查找的行
     * @param start 处理的开始位置
     * @param end   处理的结束位置
     * @param word  查找的单词
     * @return 单词出现的次数
     */
    private Integer count(String[] line, int start, int end, String word) {
        int counter;
        counter = 0;
        for (int i = start; i < end; i++) {
            if (line[i].equals(word)) {
                counter++;
            }
        }
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return counter;
    }
}
package com.concurrency.core;

import com.concurrency.task.DocumentTask;
import com.concurrency.utils.DocumentMock;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建一个模拟文档,他有100行,每行1000个单词
        // Generate a document with 100 lines and 1000 words per line
        DocumentMock mock = new DocumentMock();
        String[][] document = mock.generateDocument(100, 1000, "the");

        // 创建一个文档任务对象,处理整个文档
        DocumentTask task = new DocumentTask(document, 0, 100, "the");

        // 创建一个分合池对象
        ForkJoinPool pool = new ForkJoinPool();

        // 执行文档处理任务
        pool.execute(task);

        // 输出分合池对象的统计数据
        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());

        // 关闭分合池
        pool.shutdown();

        // 等待所有的任务完成
        try {
            pool.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出任务完成的结果
        try {
            System.out.printf("Main: The word appears %d in the document", task.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

这里写图片描述

图5.3-1 运行结果

  在这个范例中,我们实现了两个不同的任务。

  ♦ DocnmentTask类:这个类的任务需要处理由start和end属性决定的文档行如果这些行数小于10,那么,就每行创建一个LineTask对象,然后在任务执行结束后,合计返回的结果,并返回总数。如果任务要处理的行数大于10,那么,将任务拆分成两组,并创建两个DocumentTask对象来处理这两组对象。当这些任务执行结束后,同样合计返回的结果,并返回总数。

  ♦ LineTask类:这个类的任务需要处理文档中一行的某一组词。如果一组词的个数小10,那么任务将直接在这一组词里搜索特定词,然后返回査找词在这一组词中出现的次数。否则,任务将拆分这些词为两组,并创建两个LineTask对象来处理这两组词-当这 些任务执行完成后,合计返回的结果,并返回总数。
在Main主类中,我们通过默认的构造器创建了 ForkJoinPool对象,然后执行 DocumentTask类,来处理一个共有100行,每行1000字的文档。这个任务将问题拆分成DocumentTask对象和LineTask对象,然后当所有的任务执行完成后,使用原始的任务来获取整个文档中所要查找的词出现的次数。由于任务继承了 RecursheTask类,因此能 够返回结果。

  调用get()方法来获得Task返回的结果。这个方法声明在Future接口里,并由 RecursiveTask 类实现。
执行程序时,在控制台上,我们可以比较第一行与最后一行的输出信息。第一行是文档生成时被査找的词出现的次数,最后一行则是通过Fork/Join任务计算而来的被查找的词出现的次数,而且这两个数字相同。

  ForkJoinTask类提供了另一个complete()方法来结束任务的执行并返回结果。这个方法接收一个对象,对象的类型就是RecursiveTask类的泛型参数,然后在任务调用方法后返回这个对象作为结果。这一过程采用了推荐的异步任务来返回任务的结果。

  由于RecursiveTask类实现了 Future接口,因此还有get()方法调用的其他版本:

  ♦ get(long timeout, TimeUnit unit):这个版本中,如果任务的结果未准备好,将等待指定的时间。如果等待时间超出,而结果仍未准备好,那方法就会返回null值.

  TimeUnit 是一个枚举类,有如下的常量:DAYS、 HOURS、MICROSECONDS、 MILLISECONDS、 MINUTES、 NANOSECONDS 和 SECONDS。

5.4异步运行任务

  在ForkJoinPool中执行ForkJoinTask时,可以采用同步或异步方式。当采用同步方式执行时,发送任务给Fork/Join线程池的方法直到任务执行完成后才会返回结果。而采用异步方式执行时,发送任务给执行器的方法将立即返回结果,但是任务仍能够继续执行。

  需要明白这两种方式在执行任务时的一个很大的区别。当采用同步方式,调用这些方法(比如,invokeAll()方法)时,任务被挂起,直到任务被发送到Fork/Join线程池中执行完成。这种方式允许ForkJoinPool类采用工作窃取算法(Work-StealingAlgortthm)来分配一个新任务给在执行休眠任务的工作者线程CWorkerThread)。相反,当采用异步方法(比如,fork()方法)时,任务将继续执行,因此ForkJoinPool类无法使用工作窃取算 法来提升应用程序的性能。在这个示例中,只有调用join()或方法来等待任务的结束时,ForkJoinPool类才可以使用工作窃取算法。

  本节将学习如何使用ForkJoinPool和ForkJoinTask类所提供的异步方法来管理任务。我们将实现一个程序:在一个文件夹及其子文件夹中来搜索带有指定扩展名的文件。 ForkJoinTask类将实现处理这个文件夹的内容。而对于这个文件夹中的每一个子文件,任务将以异步的方式发送一个新的任务给ForkJoinPool类。对于每个文件夹中的文件,任务将检査任务文件的扩展名,如果符合条件就将其增加到结果列表中。

package com.concurrency.task;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;

/**
 * 文件夹处理类,查找指定文件夹及其子文件夹下的指定后缀名的文件
 */
public class FolderProcessor extends RecursiveTask<List<String>> {
    private static final long serialVersionUID = -6119741136325003142L;
    /**
     * 开始处理的文件目录
     */
    private String path;

    /**
     * 要查找的文件后缀名
     */
    private String extension;

    /**
     * 构造函数
     *
     * @param path      开始处理的文件目录
     * @param extension 要查找的文件后缀名
     */
    public FolderProcessor(String path, String extension) {
        this.path = path;
        this.extension = extension;
    }

    /**
     * 核心方法,查找文件夹下所有指定后缀的文件,如果是一个文件夹就开创建子线程去运行
     *
     * @return 查找到的文件集合
     */
    @Override
    protected List<String> compute() {
        List<String> list = new ArrayList<>();
        List<FolderProcessor> tasks = new ArrayList<>();
        File file = new File(path);
        File content[] = file.listFiles();
        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);
                } else {
                    if (checkFile(content[i].getName())) {
                        list.add(content[i].getAbsolutePath());
                    }
                }
            }

            if (tasks.size() > 50) {
                System.out.printf("%s: %d tasks ran.\n", file.getAbsolutePath(), tasks.size());
            }
            addResultsFromTasks(list, tasks);
        }

        return list;
    }

    /**
     * 汇总统计结果
     *
     * @param list  结果存放的集合
     * @param tasks 任务集合
     */
    private void addResultsFromTasks(List<String> list, List<FolderProcessor> tasks) {
        for (FolderProcessor item : tasks) {
            list.addAll(item.join());
        }
    }

    /**
     * 检查文件是否以指定的名称结束
     *
     * @param name 文件扩展名
     * @return true以指定的名字结束,false不以指定的名字结束
     */
    private boolean checkFile(String name) {
        return name.endsWith(extension);
    }
}
package com.concurrency.core;

import com.concurrency.task.FolderProcessor;

import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建分合池
        ForkJoinPool pool = new ForkJoinPool();

        // 为三个不同的文件夹创建文件处理器对象
        FolderProcessor system = new FolderProcessor("C:\\Windows", "log");
        FolderProcessor apps = new FolderProcessor("C:\\Program Files", "log");
        FolderProcessor documents = new FolderProcessor("C:\\Documents And Settings", "log");

        // 在分合池中执行一个任务
        pool.execute(system);
        pool.execute(apps);
        pool.execute(documents);

        // 输出统计信息,直到三个任务都完成
        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()));

        // 关闭分合池
        pool.shutdown();

        // 保存每个任务统计的结束
        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());
    }
}

这里写图片描述

图5.4-1 部分运行结果

  这个范例的重点在于FolderProccssor类。每一个任务处理一个文件夹中的内容。文件夹中的内容有以下两种类型的元素:

  ♦文件;

  ♦其他文件夹。

  如果主任务发现一个文件夹,它将创建另一个Task对象来处理这个文件夹,调用fork()方法把这个新对象发送到线程池中。fork()方法发送任务到线程池时,如果线程池中有空闲的工作者线程(WorkerThread)或者将创建一个新的线程,那么开始执行这个任务,fork()方法会立即返回,因此,主任务可以继续处理文件夹里的其他内容。对于每一个文件,任务开始比较它的文件扩展名,如果与要搜索的扩展名相同,那么将文件的完整路径增加到结果列表中。

  一旦主任务处理完指定文件夹里的所有内容,它将调用join()方法等待发送到线程池中的所有子任务执行完成,join()方法在主任务中被调用,然后等待任务执行结束,并通过 compute()方法返回值。主任务将所有的子任务结果进行合并,这些子任务发送到线程池中时带有自己的结果列表,然后通过调用compute()方法返回这个列表并作为主任务的返回值。

  ForkJoinPool类也允许以异步的方式执行任务。调用execute()方法发送3个初始任务 到线程池中,在Main主类中,调用shirtdmvnO方法结束线程池,并在控制台输出线程池中任务的状态及其变化的过程。ForkJotoPool类包含了多个方法可以实现这个目的。参考8.5节来查阅这些方法的详细列表。

  本范例使用join()方法来等待任务的结束,然后获取它们的结果。也可以使用get()方法以下的两个版本来完成这个目的。

  ♦ get():如果ForkJoinTask类执行结束,或者一直等到结束,那么get()方法的这个版本则返回由compute()方法返回的结果。

  ♦ get(long timeout, TimeUnit unit):如果任务的结果未准备好,那么get()方法的这个版本将等待指定的时间。如果超过指定的时间了,任务的结果仍未准备好,那么这个方法将返回null值。TimeUnit是一个枚举类,有如下的常量:DAYS、HOURS、 MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS 和 SECONDS。

  方法和join()方法还存在两个主要的区别:

  ♦ join()方法不能被中断,如果中断调用join〇方法的线程,方法将抛出 InterruptedException 异常;

  ♦ 如果任务抛出任何运行时异常,那么get()方法将返回ExecutionException异常,但是join()方法将返回RunlimeException异常。

5.5在任务中抛出异常

  Java有两种类型的异常。

  ♦ 非运行时异常(Checked Exception):这些异常必须在方法上通过throws子句抛出,或者在方法体内通过try{…}catch{…}方式进行捕捉处理。比如IOException或 ClassNotFoundException 异常。

  ♦ 运行时异常(Unchecked Exception):这些异常不需要在方法上通过throws子句抛出,也不需要在方法体内通过try{…}catch{…}方式进行捕捉处理。比如 NumberFormatException 异常。

  不能在ForkJoinTask类的compute()方法中抛出任何非运行时异常,因为这个方法的实现没有包含任何throws声明。因此,需要包含必需的代码来处理相关的异常。另一方面, compute()方法可以抛出运行时异常(它可以是任何方法或者方法内的对象抛出的异常)。 ForkJoinTask类和ForkJoinPool类的行为与我们期待的可能不同。在控制台上,程序没有结束执行,不能看到任何异常信息。如果异常不被抛出,那么它只是简单地将异常吞噬掉。然而,我们能够利用ForkJoinTask类的一些方法来获知任务是否有异常抛出,以及抛出哪一种类型的异常。

package com.concurrency.task;

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.TimeUnit;

/**
 * 任务执行方法
 */
public class Task extends RecursiveTask<Integer> {
    /**
     * 序列化版本号
     */
    private static final long serialVersionUID = 1L;

    /**
     * 待处理的数组
     */
    private int array[];

    /**
     * 任务处理的起始位置
     */
    private int start;
    /**
     * 任务处理的结束位置
     */
    private int end;

    /**
     * 构造函数
     *
     * @param array 待处理的数组
     * @param start 任务处理的起始位置
     * @param end   任务处理的结束位置
     */
    public Task(int array[], int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    /**
     * 核心方法,如果处理的元素大于9个就分成两个任务去执行它,如果处理的起始位置小于3,结束位置大于3就抛出异常
     */
    @Override
    protected Integer compute() {
        System.out.printf("Task: Start from %d to %d\n", start, end);
        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();
            }

        } else {
            int mid = (end + start) / 2;
            Task task1 = new Task(array, start, mid);
            Task task2 = new Task(array, mid, end);
            invokeAll(task1, task2);
            System.out.printf("Task: Result form %d to %d: %d\n", start, mid, task1.join());
            System.out.printf("Task: Result form %d to %d: %d\n", mid, end, task2.join());
        }
        System.out.printf("Task: End form %d to %d\n", start, end);
        return 0;
    }

}
package com.concurrency.core;

import com.concurrency.task.Task;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建长度为100的整形数组
        int array[] = new int[100];
        // 创建处理数组的任务
        Task task = new Task(array, 0, 100);
        // 创建分合池对象去执行这个任务
        ForkJoinPool pool = new ForkJoinPool();

        // 执行任务
        pool.execute(task);

        // 关闭分合池
        pool.shutdown();

        // 等待任务的完成
        try {
            pool.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 检查是否抛出异常,如果抛出异常就输出信息
        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());
    }
}

这里写图片描述

图5.5-1 部分运行结果

  在本节,我们实现的Task类用来处理一个数字数组。它检查要处理的数字块规模是否包含有10个或更多个元素。在这个情况下,Task类拆分这个数字块为两部分,然后创建两个新的Task对象用来处理这两部分。否则,它将寻找位于数组中第4个位置(索引位为3)的元素。如果这个元素位于任务处理块中,它将抛出一个RuntimeException异常。

  虽然运行这个程序时将抛出异常,但是程序不会停止。在Main主类中,调用原始任务ForkJoinTask类的isCompletedAbnormally()方法,如果主任务或者它的子任务之一抛出了异常,这个方法将返回true。也可以使用getException()方法来获得抛出的Exception对象。

  当任务抛出运行时异常时,会影响它的父任务(发送到ForkJoinPool类的任务),以及父任务的父任务,以此类推。查阅程序的输出结果,将会发现有一些任务没有结束的信息。
这些任务是那些抛出异常的任务和它的父任务。所有这些任务都是异常结束的。记住一点:在用ForkJoinPool对象和ForkJoinTask对象开发一个程序时,它们是会抛出异常的,如果不想要这种行为,就得采用其他方式。

  在范例中,不采用抛出异常,而调用ForkJoinTask类的completeExceptlonallyO方法也可以获得同样的结果。代码如下所示:

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

5.6取消任务

  在ForkJoinPool类中执行ForkJoinTask对象时,在任务开始执行前可以取消它。

  ForkJoinTask类提供了 cancel()方法来达到取消任务的目的。在取消一个任务时必须要注 意以下两点:

  ♦ ForkJoinPool类不提供任何方法来取消线程池中正在运行或者等待运行的所有任务;

  ♦ 取消任务时,不能取消已经被执行的任务。

  在本节,我们将实现一个取消ForkJoinTask对象的范例。该范例将寻找数组中某个数字所处的位置。第一个任务是寻找可以被取消的剩余任务数。由于Fork/Join框架没有提供取消功能,我们将创建一个辅助类来实现取消任务的操作。

package com.concurrency.utils;

import java.util.Random;

/**
 * 整形数组生成类
 */
public class ArrayGenerator {
    /**
     * 生成整形数组,生成的值在[0, size)
     * @param size 数组长度
     * @return 长度为size的数组
     */
    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;
    }
}
package com.concurrency.utils;

import com.concurrency.task.TaskManager;

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.TimeUnit;

/**
 * 数值查找类,在数组中查找指定的数值
 */
public class SearchNumberTask extends RecursiveTask<Integer> {

    /**
     * 序列化版本号
     */
    private static final long serialVersionUID = 1L;

    /**
     * 如果没有找到指定的值就返回-1
     */
    private final static int NOT_FOUND = -1;

    /**
     * 待查找的数据
     */
    private int numbers[];

    /**
     * 数组处理的开始位置
     */
    private int start;
    /**
     * 数据处理的结束位置
     */
    private int end;

    /**
     * 要查找的数值
     */
    private int number;

    /**
     * 任务管理器对象,可以对任务进行取消操作
     */
    private TaskManager manager;

    /**
     * 构造函数,初始化属性
     *
     * @param numbers 待查找的数据
     * @param start   数组处理的开始位置
     * @param end     数据处理的结束位置
     * @param number  要查找的数值
     * @param manager 任务管理器对象
     */
    public SearchNumberTask(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;
    }

    /**
     * 核心方法,如查处理的数组数目大于10就调用launchTasks()方法,否则lookForNumber()
     *
     * @return 查找到的位置
     */
    @Override
    protected Integer compute() {
        System.out.println("Task: " + start + ":" + end);
        int ret;
        if (end - start > 10) {
            ret = launchTasks();
        } else {
            ret = lookForNumber();
        }
        return ret;
    }

    /**
     * 数据查找方法,找出[start, end)中第一次number出现的位置
     *
     * @return [start, end)中第一次number出现的位置
     */
    private int lookForNumber() {
        for (int i = start; i < end; i++) {
            if (numbers[i] == number) {
                System.out.printf("Task: Number %d found in position %d\n", number, i);
                manager.cancelTasks(this); // 取消任务的执行
                return i;
            }
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return NOT_FOUND;
    }


    /**
     * 运行任务的方法,分成 两个任务运行
     *
     * @return [start, end)中第一次number出现的位置
     */
    private int launchTasks() {
        int mid = (start + end) / 2;

        SearchNumberTask task1 = new SearchNumberTask(numbers, start, mid, number, manager);
        SearchNumberTask task2 = new SearchNumberTask(numbers, mid, end, number, manager);

        manager.addTask(task1);
        manager.addTask(task2);

        task1.fork();
        task2.fork();
        int returnValue;

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

        returnValue = task2.join();
        return returnValue;
    }

    public void writeCancelMessage() {
        System.out.printf("Task: Cancelled task from %d to %d\n", start, end);
    }
}
package com.concurrency.task;

import com.concurrency.utils.SearchNumberTask;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinTask;

/**
 * 任务管理类
 */
public class TaskManager {
    /**
     * 任务列表对象
     */
    private List<ForkJoinTask<Integer>> tasks;

    /**
     * 构造函数,初始化任务值列表对象
     */
    public TaskManager(){
        tasks=new ArrayList<>();
    }

    /**
     * 在列表中添加一个新的任务
     * @param task 新的任务
     */
    public void addTask(ForkJoinTask<Integer> task){
        tasks.add(task);
    }

    /**
     * 取消队列中的指定任务
     * @param cancelTask 指定的任务
     */
    public void cancelTasks(ForkJoinTask<Integer> cancelTask){
        for (ForkJoinTask<Integer> task  :tasks) {
            if (task!=cancelTask) {
                task.cancel(true);
                ((SearchNumberTask)task).writeCancelMessage();
            }
        }
    }
}
package com.concurrency.core;

import com.concurrency.task.TaskManager;
import com.concurrency.utils.ArrayGenerator;
import com.concurrency.utils.SearchNumberTask;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建一个数组生成器对象,生成一个长度为1000的整形数组
        ArrayGenerator generator = new ArrayGenerator();
        int array[] = generator.generateArray(1000);

        // 创建一个任务管理对象
        TaskManager manager = new TaskManager();

        // 使用默认的构造函数,创建一个分合池对象
        ForkJoinPool pool = new ForkJoinPool();

        // 创建一个处理任务的数组
        SearchNumberTask task = new SearchNumberTask(array, 0, 1000, 5, manager);

        //执行任务
        pool.execute(task);

        // 关闭这个池
        pool.shutdown();


        // 等待任务完成
        try {
            pool.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出信息,表示程序已经完成
        System.out.printf("Main: The program has finished\n");
    }
}

这里写图片描述

图5.6-1 部分运行结果

  ForkJoinTask类提供的cancel()方法允许取消一个仍没有被执行的任务,这是非常重要的一点。如果任务己经开始执行,那么调用cancel()方法也无法取消。这个方法接收一个名为mayInterruptIfRunning的boolean值参数。顾名思义,如果传递true值给这个方法,即使任务正在运行也将被取消。Java API文档指出,ForkJoinTask类的默认实现,这个属性没有起作用。如果任务还没有开始执行,那么这些任务将被取消。任务的取消对于已发送到线程池中的任务没有影响,它们将继续执行。

  Fork/Join框架的局限性在于,ForkJointPool线程池中的任务不允许被取消。为了克服这种局限性,我们实现了TaskManager类,它存储发送到线程池中的所有任务,可以用一个方法来取消存储的所有任务。如果任务正在运行或者已经执行结束,那么任务就不能被取消,cancel()方法返回false值,因此可以尝试去取消所有的任务而不用担心可能带来的间接影响。

  这个范例实现在数字数组中寻找一个数字。根据Fork/Join框架的推荐,我们将问题拆分为更小的子问题。由于我们仅关心数字的一次出现,因此,当找到它时,就会取消其他的所有任务。

欢迎转载,转载请注明出处http://blog.csdn.net/derrantcm/article/details/48192473

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值