概述
Fork/Join框架是一个Java 7提供的用于并行执行任务的框架,是一个把大型任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取算法
工作窃取算法是指某个线程从其他队列里窃取任务来执行。使用工作窃取算法可以充分利用线程进行并行计算,减少了线程间的竞争。但是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源,比如创建多个线程和双端队列。
Fork/Join的设计
1. 分割任务。需要有一个fork类把大任务分割成子任务,如果子任务还是很大,就需要不停地分割,直到分割出的子任务足够小。
2. 执行任务并合并结果。分割的子任务放在双端队列里,然后几个启动线程分别从双端队里里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队里拿数据,然后合并这些数据。
Fork/Join使用两个类来完成以上的两件事情:
(1) ForkJoinTask:使用Fork/Join框架,必须首先创建一个ForkJoin任务。她提供在任务中执行fork()和join()操作的机制。通常情况下,不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
RecursiveAction:用于没有返回结果的任务
RecursiveTask:用于有返回结果的任务
(2)ForkJoinPool:这个类实现了ExecutorService接口和工作窃取算法(Work-Stealing Algorithm)。它管理工作者线程,并提供任务的状态信息,以及任务的执行信息ForkJoinTask需要通过ForkJoinPool来执行
分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务
创建Fork/Join线程池
在本节,我们将学习如何使用Fork/Join框架的基本元素。它包括:
- 创建用来执行任务的ForkJoinPool对象;
- 创建即将在线程池中被执行的任务ForkJoinTask子类。
本范例中即将使用的Fork/Join框架的主要特性如下:
- 采用默认的构造器创建ForkJoinPool对象;
- 在任务中将使用JavaAPI文档推荐的结构。
if (problem size > default size){
tasks=divide(task);
execute(tasks);
} else {
resolve problem using another algorithm;}
- 我们将以同步的方式执行任务。当一个主任务执行两个或更多的子任务时,这个主任务将等待子任务的完成。用这种方法,执行主任务的线程,称之为工作者线程(Worker Thread),它将寻找其他的子任务来执行,并在子任务执行的时间里利用所有的线程优势。
- 如果将要实现的任务没有返回任何结果,那么,采用RecursiveAction类作为实现任务的基类。
范例实现
在本节,我们将实现一项更新产品价格的任务。最初的任务将负责更新列表中的所有元素。我们使用10来作为参考大小(ReferenceSize),如果一个任务需要更新大于10个元素,它会将这个列表分解成为两部分,然后分别创建两个任务用来更新各自部分的产品价格。
创建Product类
public class Product {
private String name;
private double price;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the price
*/
public double getPrice() {
return price;
}
/**
* @param price the price to set
*/
public void setPrice(double price) {
this.price = price;
}
}
创建一个名为ProductListGenerator的类,用来生成一个随机产品列表。
public class ProductListGenerator {
/**
* 接收一个表示列表大小的int参数,并返回一个生成产品的List列表
* @param size
* @return
*/
public List<Product> generate (int size) {
List<Product> ret=new ArrayList<Product>();
for (int i=0; i<size; i++){
Product product=new Product();
product.setName("Product "+i);
product.setPrice(10);
ret.add(product);
}
return ret;
}
}
创建一个名为Task的类,并继承RecursiveAction类
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
public class Task extends RecursiveAction {
/**
*
*/
private static final long serialVersionUID = 1L;
private List<Product> products ;
/**
* 声明两个私有的int属性,分别命名为first和last。这两个属性将决定任务执行时对产品的分块
*/
private int first;
private int last;
/**
* 声明一个名为increment的私有double属性,用来存储产品价格的增加额。
*/
private double increment;
public Task(List<Product> products,int first,int last,double increment){
this.products=products;
this.first=first;
this.last=last;
this.increment=increment;
}
/**
* 这个方法用来更新在产品列表中处于first和last属性之间的产品。
*/
private void updatePrices() {
for (int i=first; i<last; i++){
Product product=products.get(i);
product.setPrice(product.getPrice()*(1+increment));
}
}
@Override
protected void compute() {
//如果last和first属性值的差异小于10(一个任务只能更新少于10件产品的价格),则调用updatePrices()方法增加这些产品的价格。
if (last-first<10) {
updatePrices();
} else {//如果last和first属性值的差异大于或等于10,
//就创建两个新的Task对象,一个处理前一半的产品,
//另一个处理后一半的产品,然后调用ForkJoinPool的invokeAll()方法来执行这两个新的任务
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);
}
}
public static void main(String[] args){
ProductListGenerator productListGenerator=new ProductListGenerator();
List<Product> products=productListGenerator.generate(1000);
Task task = new Task(products, 0, products.size(),0.5);
ForkJoinPool forkJoinPool = new ForkJoinPool();
//调用execute()方法执行任务。
forkJoinPool.execute(task);
//显示关于线程池演变的信息,每5毫秒在控制台上输出线程池的一些参数值,直到任务执行结束
do {
System.out.println(" Main : Thread Count : " + forkJoinPool.getActiveThreadCount());//打印运行的线程数
System.out.println(" Main : Thread Steal : " + forkJoinPool.getStealCount());//窃取的线程数
System.out.println(" Main : Parallelism : " + forkJoinPool.getParallelism()); //并发
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!task.isDone());
//调用shutdown()方法关闭线程池
forkJoinPool.shutdown();
//调用isCompletedNormally()方法,检查任务是否已经完成并且没有错误
if(task.isCompletedNormally()){
System.out.println("Main: The process has completed normally.\n");
}
//在增加之后,所有产品的期望价格是15元。
//在控制台输出所有产品的名称和价格,
//如果产品的价格不是15元,就将产品信息打印出来,
//以便确认所有的产品价格都正确地增加了
for (int i=0; i<products.size(); i++){
Product product=products.get(i);
if (product.getPrice()!=15) {
System.out.printf("Product %s: %f\n",product.getName(),product.getPrice());
}
}
System.out.println("Main: End of the program.\n");
}
}
在这个范例中,我们创建了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类的一些方法,来检查正在运行的任务的状态和演变情况。这个类包含更多的方法,可以用于任务状态的检测
最后,像执行器框架一样,必须调用shutdown()方法来结束ForkJoinPool的执行
ForkJoinPool类还提供了以下方法用于执行任务。
- execute (Runnabletask):这是本范例中使用的execute()方法的另一种版本。这个方法发送一个Runnable任务给ForkJoinPool类。需要注意的是,使用Runnable对象时ForkJoinPool类就不采用工作窃取算法(Work-StealingAlgorithm),ForkJoinPool类仅在使用ForkJoinTask类时才采用工作窃取算法。
- invoke(ForkJoinTasktask):正如范例所示,ForkJoinPool类的execute()方法是异步调用的,而ForkJoinPool类的invoke()方法则是同步调用的。这个方法直到传递进来的任务执行结束后才会返回。
- 也可以使用在ExecutorService类中声明的invokeAll()和invokeAny()方法,这些方法接收Callable对象作为参数。使用Callable对象时ForkJoinPool类就不采用工作窃取算法(Work-StealingAlgorithm),因此,最好使用执行器来执行Callable对象。
ForkJoinTask类也包含了在范例中所使用的invokeAll()方法的其他版本,这些版本如下。
- invokeAll(ForkJoinTask< ? >… tasks):这个版本的方法接收一个可变的参数列表,可以传递尽可能多的ForkJoinTask对象给这个方法作为参数。
- invokeAll(Collectiontasks):这个版本的方法接受一个泛型类型T的对象集合(比如,ArrayList对象、LinkedList对象或者TreeSet对象)。这个泛型类型T必须是ForkJoinTask类或者它的子类。
虽然ForkJoinPool类是设计用来执行ForkJoinTask对象的,但也可以直接用来执行Runnable和Callable对象。当然,也可以使用ForkJoinTask类的adapt()方法来接收一个Callable对象或者一个Runnable对象,然后将之转化为一个ForkJoinTask对象,然后再去执行。
合并任务的结果
Fork/Join框架提供了执行任务并返回结果的能力。这些类型的任务都是通过RecursiveTask类来实现的。RecursiveTask类继承了ForkJoinTask类,并且实现了由执行器框架(Executor Framework)提供的Future接口。
在任务中,必须使用Java API文档推荐的如下结构:
if (problem size > size){
tasks=Divide(task);
execute(tasks);
groupResults()
return result;
} else {
resolve problem;
return result;
}
如果任务需要解决的问题大于预先定义的大小,那么就要将这个问题拆分成多个子任务,并使用Fork/Join框架来执行这些子任务。执行完成后,原始任务获取到由所有这些子任务产生的结果,合并这些结果,返回最终的结果。当原始任务在线程池中执行结束后,将高效地获取到整个问题的最终结果。
接下来开发一个应用程序,在文档中查找一个词。我们将实现以下两种任务:
一个文档任务,它将遍历文档中的每一行来查找这个词;
一个行任务,它将在文档的一部分当中查找这个词。
所有这些任务将返回文档或行中所出现这个词的次数。
创建一个名为DocumentMock的类。它将生成一个字符串矩阵来模拟一个文档:
import java.util.Random;
public class DocumentMock {
/**
* 用一些词来创建一个字符串数组。这个数组将被用来生成字符串矩阵
*/
private String words[]={"the","hello","goodbye","packt", "java",
"thread","pool","random","class","main"};
/**
* 它接收3个参数,分别是行数numLines,每一行词的个数numWords,和准备查找的词word。然后返回一个字符串矩阵
* @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();
//为字符串矩阵填上字符串。通过随机数取得数组words中的某一字符串,
//然后存入到字符串矩阵document对应的位置上,同时计算生成的字符
//串矩阵中将要查找的词出现的次数。这个值可以用来与后续程序运行查
//找任务时统计的次数相比较,检查两个值是否相同
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.println("DocumentMock: The word appears "+counter+" times in the document");
return document;
}
}
创建名为LineTask的类,并继承RecursiveTask类,RecursiveTask类的泛型参数为Integer类型。这个RecursiveTask类实现了一个任务,用来计算所要查找的词在一行中出现的次数。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;
/**
* 用来计算所要查找的词在一行中出现的次数
*
*/
public class LineTask extends RecursiveTask<Integer> {
/**
*
*/
private static final long serialVersionUID = 1L;
private String line[];
private int start, end;
private String word;
public LineTask(String line[], int start, int end, String word){
this.line=line;
this.start=start;
this.end=end;
this.word=word;
}
private Integer count(String[] line, int start, int end, String word) {
int counter;
counter=0;
//将存储在start和end属性值之间的词与任务正在查找的word属性相比较。如果相同,那么将计数器counter变量加1
for (int i=start; i<end; i++){
if (line[i].equals(word)){
counter++;
}
}
try {
Thread.sleep(10);//为了延缓范例的执行,将任务休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return counter;
}
/**
* 计算两个数字之和并返回结果
* @param number1
* @param number2
* @return
*/
private Integer groupResults(Integer number1, Integer number2) {
Integer result;
result=number1+number2;
return result;
}
/**
* 如果end和start属性的差异小于100,那么任务将采用count()方法,
* 在由start与end属性所决定的行的片断中查找词
*/
@Override
protected Integer compute() {
Integer result=null;
if (end-start<100) {//如果end和start属性的差异小于100,那么任务将采用count()方法
result=count(line, start, end, word);
} else {//如果end和start属性的差异不小于100,将这一组词拆分成两组,
//然后创建两个新的LineTask对象来处理这两个组,调用invokeAll()方法在线程池中执行它们
int mid=(start+end)/2;
LineTask task1=new LineTask(line, start, mid, word);
LineTask task2=new LineTask(line, mid, end, word);
invokeAll(task1, task2);
try {
result=groupResults(task1.get(),task2.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
}
}
创建名为DocumentTask的类,并继承RecursiveTask类,RecursiveTask类的泛型参数为Integer类型。这个DocumentTask类将实现一个任务,用来计算所要查找的词在行中出现的次数。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.TimeUnit;
/**
* 创建名为DocumentTask的类,并继承RecursiveTask类,
* RecursiveTask类的泛型参数为Integer类型。
* 这个DocumentTask类将实现一个任务,用来计算所要查找的词在行中出现的次数
*
*/
public class DocumentTask extends RecursiveTask<Integer> {
private String document[][];
private int start, end;
private String word;
public DocumentTask (String document[][], int start, int end, String word){
this.document=document;
this.start=start;
this.end=end;
this.word=word;
}
/**
* 实现processLines()方法。接收4个参数,一个字符串document矩阵,start属性,end属性和任务将要查找的词word的属性
*/
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 (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);
}
private Integer groupResults(Integer number1, Integer number2) {
Integer result;
result=number1+number2;
return result;
}
/**
* 实现compute()方法。如果end和start的差异小于10,则调用processLines()方法,来计算这两个位置之间要查找的词出现的次数
*/
@Override
protected Integer compute() {
int result = 0;
//如果end和start的差异小于10,则调用processLines()方法,来计算这两个位置之间要查找的词出现的次数
if (end-start<10){
result=processLines(document, start, end, word);
//否则,拆分这些行成为两个对象,并创建两个新的DocumentTask对象来处理这两个对象,然后调用invokeAll()方法在线程池里执行它们。
} 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 {
//采用groupResults()方法将这两个任务返回的值相加。最后,返回任务计算的结果。
result=groupResults(task1.get(),task2.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
}
}
实现范例的主类,创建DocumentMain主类,并实现main()方法
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
public class DocumentMain {
public static void main(String args[]){
//创建Document对象,包含100行,每行1,000个词。
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();
//调用awaitTermination()等待任务执行结束
try {
pool.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在控制台输出文档中出现要查找的词的次数。检验这个数字与DocumentMock类输出的数字是否一致
try {
System.out.printf("Main: The word appears %d in the document",task.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
工作原理
在这个范例中,我们实现了两个不同的任务。
- DocumentTask类:这个类的任务需要处理由start和end属性决定的文档行。如果这些行数小于10,那么,就每行创建一个LineTask对象,然后在任务执行结束后,合计返回的结果,并返回总数。如果任务要处理的行数大于10,那么,将任务拆分成两组,并创建两个DocumentTask对象来处理这两组对象。当这些任务执行结束后,同样合计返回的结果,并返回总数。
- LineTask类:这个类的任务需要处理文档中一行的某一组词。如果一组词的个数小100,那么任务将直接在这一组词里搜索特定词,然后返回查找词在这一组词中出现的次数。否则,任务将拆分这些词为两组,并创建两个LineTask对象来处理这两组词。当这些任务执行完成后,合计返回的结果,并返回总数。
在DocumentMain主类中,我们通过默认的构造器创建了ForkJoinPool对象,然后执行DocumentTask类,来处理一个共有100行,每行1,000字的文档。这个任务将问题拆分成DocumentTask对象和LineTask对象,然后当所有的任务执行完成后,使用原始的任务来获取整个文档中所要查找的词出现的次数。由于任务继承了RecursiveTask类,因此能够返回结果。
调用get()方法来获得Task返回的结果。这个方法声明在Future接口里,并由RecursiveTask类实现。
执行程序时,在控制台上,我们可以比较第一行与最后一行的输出信息。第一行是文档生成时被查找的词出现的次数,最后一行则是通过Fork/Join任务计算而来的被查找的词出现的次数,而且这两个数字相同。
ForkJoinTask类提供了另一个complete()方法来结束任务的执行并返回结果。这个方法接收一个对象,对象的类型就是RecursiveTask类的泛型参数,然后在任务调用join()方法后返回这个对象作为结果。这一过程采用了推荐的异步任务来返回任务的结果。
由于RecursiveTask类实现了Future接口,因此还有get()方法调用的其他版本:
get(long timeout, TimeUnit unit):这个版本中,如果任务的结果未准备好,将等待指定的时间。如果等待时间超出,而结果仍未准备好,那方法就会返回null值。
TimeUnit是一个枚举类,有如下的常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
异步运行任务
在ForkJoinPool中执行 ForkJoinTask时,可以采用同步或异步方式。当采用同步方式执行时,发送任务给Fork/Join线程池的方法直到任务执行完成后才会返回结果。而采用异步方式执行时,发送任务给执行器的方法将立即返回结果,但是任务仍能够继续执行。
需要明白这两种方式在执行任务时的一个很大的区别。当采用同步方式,调用这些方法(比如,invokeAll()方法)时,任务被挂起,直到任务被发送到Fork/Join线程池中执行完成。这种方式允许ForkJoinPool类采用工作窃取算法(Work-StealingAlgorithm)来分配一个新任务给在执行休眠任务的工作者线程(WorkerThread)。相反,当采用异步方法(比如,fork()方法)时,任务将继续执行,因此ForkJoinPool类无法使用工作窃取算法来提升应用程序的性能。在这个示例中,只有调用join()或get()方法来等待任务的结束时,ForkJoinPool类才可以使用工作窃取算法。
本节将学习如何使用ForkJoinPool和ForkJoinTask类所提供的异步方法来管理任务。
我们将实现一个程序:在一个文件夹及其子文件夹中来搜索带有指定扩展名的文件。ForkJoinTask类将实现处理这个文件夹的内容。而对于这个文件夹中的每一个子文件,任务将以异步的方式发送一个新的任务给ForkJoinPool类。对于每个文件夹中的文件,任务将检查任务文件的扩展名,如果符合条件就将其增加到结果列表中。
创建名为FolderProcessor的类,并继承RecursiveTask类,RecursiveTask类的泛型参数为List类型
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 = 1L;
/**
* 用来存储任务将要处理的文件夹的完整路径
*/
private String path;
/**
* 用来存储任务将要查找的文件的扩展名
*/
private String extension;
public FolderProcessor (String path, String extension) {
this.path=path;
this.extension=extension;
}
@Override
protected List<String> compute() {
//用来存储文件夹中文件的名称。
List<String> list=new ArrayList<>();
//用来存储子任务,这些子任务将处理文件夹中的子文件夹
List<FolderProcessor> tasks=new ArrayList<>();
File file=new File(path);
File content[] = file.listFiles();
//对于文件夹中的每一个元素,如果它是子文件夹,
//就创建一个新的FolderProcessor对象,
//然后调用fork()方法采用异步方式来执行它
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 {//调用checkFile()方法来比较文件的扩展名。
//如果文件的扩展名与将要搜索的扩展名相同,
//就将文件的完整路径存储到list
if (checkFile(content[i].getName())){
list.add(content[i].getAbsolutePath());
}
}
}
//如果FolderProcessor子任务列表超过50个元素,那么就在控制台输出一条信息表示这种情景
if (tasks.size()>50) {
System.out.printf("%s: %d tasks ran.\n",file.getAbsolutePath(),tasks.size());
}
//调用addResultsFromTask()辅助方法。它把通过这个任务而启动的子任务返回的结果增加到文件列表中。
//传递两个参数给这个方法,一个是字符串列表list,一个是FolderProcessor子任务列表tasks
addResultsFromTasks(list,tasks);
}
return list;
}
/**
* 遍历任务列表中存储的每一个任务,调用join()方法等待任务执行结束,
* 并且返回任务的结果。然后,调用addAll()方法将任务的结果增加到字符串列表中。
* @param list
* @param tasks
*/
private void addResultsFromTasks(List<String> list,List<FolderProcessor> tasks) {
for (FolderProcessor item: tasks) {
list.addAll(item.join());
}
}
/**
* 这个方法检查作为参数而传递进来的文件名,如果是以正在搜索的文件扩展名为结尾,那么方法就返回true,否则就返回false
* @param name
* @return
*/
private boolean checkFile(String name) {
return name.endsWith(extension);
}
}
实现范例的主类,创建FolderMain主类,并实现main()方法
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
public class FolderMain {
public static void main(String[] args) {
ForkJoinPool pool=new ForkJoinPool();
//创建3个FolderProcessor任务,并使用不同的文件夹路径来初始化这些任务
FolderProcessor system=new FolderProcessor("C:\\Windows","log");
FolderProcessor apps=new FolderProcessor("C:\\Program Files","log");
FolderProcessor documents=new FolderProcessor("C:\\Documents And Settings","log");
//调用execute()方法执行线程池里的3个任务
pool.execute(system);
pool.execute(apps);
pool.execute(documents);
//在控制台上每隔1秒钟输出线程池的状态信息,直到这3个任务执行结束
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()));
//调用shutdown()方法关闭ForkJoinPool线程池。
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();
}
}
工作原理
这个范例的重点在于FolderProcessor类。每一个任务处理一个文件夹中的内容。文件夹中的内容有以下两种类型的元素:
- 文件
- 其他文件夹
如果主任务发现一个文件夹,它将创建另一个Task对象来处理这个文件夹,调用fork()方法把这个新对象发送到线程池中。fork()方法发送任务到线程池时,如果线程池中有空闲的工作者线程(WorkerThread)或者将创建一个新的线程,那么开始执行这个任务,fork()方法会立即返回,因此,主任务可以继续处理文件夹里的其他内容。对于每一个文件,任务开始比较它的文件扩展名,如果与要搜索的扩展名相同,那么将文件的完整路径增加到结果列表中。
一旦主任务处理完指定文件夹里的所有内容,它将调用join()方法等待发送到线程池中的所有子任务执行完成。join()方法在主任务中被调用,然后等待任务执行结束,并通过compute()方法返回值。主任务将所有的子任务结果进行合并,这些子任务发送到线程池中时带有自己的结果列表,然后通过调用compute()方法返回这个列表并作为主任务的返回值。
ForkJoinPool类也允许以异步的方式执行任务。调用execute()方法发送3个初始任务到线程池中。在Main主类中,调用shutdown()方法结束线程池,并在控制台输出线程池中任务的状态及其变化的过程。ForkJoinPool类包含了多个方法可以实现这个目的。
本范例使用join()方法来等待任务的结束,然后获取它们的结果。也可以使用get()方法以下的两个版本来完成这个目的。
- get():如果ForkJoinTask类执行结束,或者一直等到结束,那么get()方法的这个版本则返回由compute()方法返回的结果。
- get(long timeout, TimeUnit unit):如果任务的结果未准备好,那么get()方法的这个版本将等待指定的时间。如果超过指定的时间了,任务的结果仍未准备好,那么这 个方法将返回null值。TimeUnit是一个枚举类,有如下的常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
get()方法和join()方法还存在两个主要的区别:
- join()方法不能被中断,如果中断调用join()方法的线程,方法将抛出InterruptedException异常;
- 如果任务抛出任何运行时异常,那么get()方法将返回ExecutionException异常,但是join()方法将返回RuntimeException异常。
Fork/Join的异常处理
ForkJoinTask在执行的时候可能会抛出异常,但是没法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或者已经被取消了,并且可以通过ForkJoinTask的getException方法来获取异常
if(task.isCompletedAbnormally){
System.out.println(task.getException())
}
Fork/Join的实现原理
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提供提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
(1)ForkJoinTask的fork()方法的实现原理
当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoiWorkerThread的pushTask方法异步地执行这个任务,然后立即返回结果。
public final ForkJoinTask<V> fork() {
((ForkJoinWorkerThread) Thread.currentThread())
.pushTask(this);
return this;
}
pushTask方法把当前任务存放在ForkJoinTask数组队列里。然后调用ForkJoinPool的signalWork()方法唤醒或者传一个工作线程来执行。
final void pushTask(ForkJoinTask<?> t) {
ForkJoinTask<?>[] q; int s, m;
if ((q = queue) != null) { //如果队列删除,则忽略
//queueTop 要推送或弹出的下一个队列插槽的索引(取模queue.length)
long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
UNSAFE.putOrderedObject(q, u, t);
queueTop = s + 1; // or use putOrderedInt
//最小有效队列槽的索引( queue.length取模),它始终是从非空中偷取的下一个位置。
if ((s -= queueBase) <= 2)
pool.signalWork();
else if (s == m)
growQueue();//创建或双倍扩展队列数组。 通过从旧数组窃取(deqs)并将最旧的数组放入新数组中来传递元素。
}
}
(2)ForkJoin的join方法实现原理
Join方法的主要作用是阻塞当前线程并等待获取结果。代码如下:
public final V join() {
if (doJoin() != NORMAL)
return reportResult();//返回非正常执行的结果
else
return getRawResult();//getRawResult就算任务是非正常执行结果,也会返回,或者不知道执行结果时,直接返回null
}
首先,它调用了doJoin方法,得到当前任务的状态来判断任务的结果,任务状态有4中:已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)、出现异常(EXCEPTIONAL)
如果任务状态是已完成,则直接返回任务结果
如果任务状态是被取消,则直接抛出CancellationException
如果任务状态是抛出异常,则直接抛出对应的异常
doJoin方法代码如下:
private int doJoin() {
Thread t; ForkJoinWorkerThread w; int s; boolean completed;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
if ((s = status) < 0)//说明任务以执行完成
return s;//直接返回任务状态
if ((w = (ForkJoinWorkerThread)t).unpushTask(this)) {//从ForkJoinTask数组取出任务
try {
completed = exec();//立即执行任务
} catch (Throwable rex) {
return setExceptionalCompletion(rex);//设置任务状态为EXCEPTIONAL
}
if (completed)//任务已完成
return setCompletion(NORMAL);//设置任务状态为NORMAL
}
return w.joinTask(this)//可能运行一些任务,直到当前任务完成
}
else
return externalAwaitDone();//阻塞非工作线程,直到完成
}