fork/join框架能够执行返回结果的任务,这种任务由RecursiveTask类实现,此类继承ForkJoinTask类并且实现由执行器框架提供的Future接口。
在任务中,需要使用Java API文档推荐的程序结构:
if (problem size > size){
tasks=Divide(task);
execute(tasks);
joinResults()
return result;
} else {
resolve problem;
return result;
}
如果任务需要解决的问题规模比预先定义的大,则将问题拆分成很多子任务,并且使用fork/join框架执行这些子任务。当完成执行时,初始任务得到所有子任务生成的结果,将它们进行分组,并返回最终结果。最后,当初始任务在线程池中完成执行时,能够高效的得到整个问题的最终结果。
在本节中,将通过开发在文档中检索指定词的应用来学习如何使用fork/join框架来解决这种问题。本范例中需要实现如下两种任务:
- 文件任务,用来在文档的行集合中寻找指定词
- 行任务,用来在文档特定部分中寻找指定词
所有任务将返回在处理的部分文档或行中指定词出现的次数。本节将使用Java并发API提供的默认fork/join池。
实现过程
通过如下步骤实现范例:
-
创建名为DocumentMock的类,生成一个字符串矩阵来模拟文件:
public class DocumentMock {
-
用一些词语创建字符串数组,此数组将在字符串矩阵生成中用到:
private String words[] = {"the", "hello", "goodbye", "packet", "java", "thread", "pool", "random", "class", "main"};
-
实现generateDocument()方法,接收三个参数,分别是行数,每行的字数,以及本范例中将要检索的词语,返回一个字符串矩阵:
public String[][] generateDocument(int numLines, int numWords, String word){
-
首先,创建必要的对象来生成文件–String矩阵和Random对象来生成随机数:
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 --"+ word+" -- appears " + counter + " times in the document\n"); return document; }
-
创建名为DocumentTask的类,继承Integer类参数化的RecursiveTask类。用来实现在行集合中检索词出现次数的任务:
public class DocumentTask extends RecursiveTask<Integer>{
-
定义名为document的私有String矩阵,以及两个名为start和end的私有int属性,定义名为word的私有String属性:
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; }
-
实现compute()方法,如果属性end和start差小于10,此任务通过调用processLines()方法在这两个属性之间位置的行中计算检索词出现的次数:
@Override protected Integer compute() { Integer result = null; 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);
-
然后,使用groupResults()方法添加两个任务的返回值。最后,返回任务计算出的结果:
try { result = groupResults(task1.get(), task2.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } return result; }
-
实现processLines()方法,参数包括字符串矩阵,start属性,end属性,以及任务检索词word属性:
private Integer processLines(String[][] document, int start, int end, String word) {
-
对任务需要处理的每一行,创建LineTask对象来处理完整行并且将它们存储到任务列表中:
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()方法执行列表中的所有任务:
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 result; }
-
实现groupResults()方法,两个数相加并返回结果:
private Integer groupResults(Integer number1, Integer number2) { Integer result ; result = number1 + number2; return result; }
-
创建名为LineTask的类,继承Integer类参数化的RecursiveTask类。此类实现在一行中计算检索词出现次数的任务:
public class LineTask extends RecursiveTask<Integer>{
-
定义名为line的私有String数组属性,和两个名为start和end的私有int属性。最后,定义名为word的私有String属性:
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; }
-
实现compute()方法,如果属性end和start差小于100,此任务通过调用count()方法在这两个属性之间位置的行中寻找检索词:
@Override protected Integer compute() { Integer result = null; if(end - start < 100) { result = count(line, start, end, word);
-
否则,将行中的词语分成两组,创建两个LineTask对象来处理这两组词语,并且在线程池中使用invokeAll()方法来执行它们:
}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);
-
然后,使用groupResults()方法添加两个任务的返回值。最后,返回任务计算出的结果:
try { result = groupResults(task1.get(), task2.get()); } catch (InterruptedException | ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return result; }
-
实现count()方法,参数包括字符串数组,start属性,end属性,以及任务检索词word属性:
private Integer count(String[] line, int start, int end, String word) {
-
将字符串数组中在start和end属性位置之间存储的词语与检索词word属性比较,如果相等的话,增加counter变量值:
int counter = 0; for(int i = start ; i < end ; i ++){ if(line[i].equals(word)){ counter ++; } }
-
设置任务休眠10毫秒来减慢范例执行:
try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
-
返回couter变量值:
return counter; }
-
实现groupResults()方法,两个数相加并返回结果:
private Integer groupResults(Integer number1, Integer number2) { Integer result; result = number1 + number2; return result; }
-
实现范例的主方法,创建一个包含main()方法的Main类:
public class Main { public static void main(String[] args) {
-
使用DocumentMock类创建100行每行1000词的Document对象:
DocumentMock mock = new DocumentMock(); String word = "java"; String[][] document = mock.generateDocument(100, 1000, word);
-
创建DocumentTask对象更新整个文件的产品,start参数赋值0,end参数赋值100:
DocumentTask task = new DocumentTask(document, 0, 100, word);
-
使用commonPool()得到默认的ForkJoinPool执行器,然后使用execute()方法在此执行器中运行任务:
ForkJoinPool commonPool = ForkJoinPool.commonPool(); commonPool.execute(task);
-
实现展示线程池处理进度信息的代码块,在任务完成执行之前,每隔一秒输出线程池的参数值到控制台:
do{ System.out.printf("************************************\n"); System.out.printf("Main : Active Threads : %d\n", commonPool.getActiveThreadCount()); System.out.printf("Main : Task Count : %d\n", commonPool.getQueuedTaskCount()); System.out.printf("Main : Steal Count : %d\n", commonPool.getStealCount()); System.out.printf("************************************\n"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }while (!task.isDone());
-
使用shutdown()方法关闭线程池:
commonPool.shutdown();
-
使用awaitTermination()方法等待任务结束:
try { commonPool.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }
-
输出文件中检索词出现的次数到控制台,检查此数字与通过DocumentMock类写入的数字是否相同:
try { System.out.printf("Main : The word --"+ word+" -- appears %d in the document", task.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }
工作原理
本范例中,实现两个不同的任务:
- DocumentTask:此类的一个任务需要处理文件中由start和end属性确定的行集合。如果行集合长度小于10,则为每行创建LineTask,当所有任务结束执行时,将这些任务的结果相加并返回求和结果。如果任务需要处理的行集长度大于等于10,则拆分成两个集合,并且创建两个DocumentTask对象分别处理这两个新的集合。当这些任务结束执行时,将各自结果相加并返回求和结果。
- LineTask:此类的一个任务需要处理文件中指定行的一组词。如果数量小于100,任务直接检索这组词并返回检索词的出现次数。否则,拆分成两组词并且创建两个LineTask对象分别处理这两个集合。当这些任务结束执行时,将各自结果相加并返回求和结果。
在Main类中,用到默认的ForkJoinPool并且在其中执行DocumentTask类来处理一个包含100行,每行1000个词的文件。此任务将问题拆分成其它的DocumentTask对象和LineTask对象,当所有任务结束执行时,使用初始任务得到整个文件中检索词出现的总次数。由于这些任务返回了结果,所以继承RecursiveTask类。
为了获得通过Task返回的结果,用到了get()方法,此方法在RecursiveTask类实现的Future接口中定义。
当执行本范例时,可以比较输出控制台第一行和最后一行的信息。第一行显示文件生成时计算出的检索词出现次数,最后一行显示通过fork/join任务计算出的相同结果。
扩展学习
complete()方法是ForkJoinTask类提供的另一种用来结束任务执行并返回结果的方法。此方法接受一个在RecursiveTask类参数化中用到的类型对象,并且当调用join()方法时,将这个对象作为任务结果返回。建议在异步认为中用此方法提供结果。
因为RecursiveTask类实现了Future接口,所以get()方法还有另一种形式:
- get(long timeout, TimeUnit unit):在这个方法中,如果任务的结果无效,则等待指定的时间,如果已过指定时间且结果依然无效,此方法返回null值。TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、和SECONDS。
更多关注
-
本章“创建fork/join池”小节
-
第九章“测试并发应用”中的“监控fork/join池”小节。