在ForkJoinPool中可以使用同步或者异步的方式来执行ForkJoinTask。当使用同步方式时,发送任务到线程池中的方法直到此任务发送结束执行时才返回。当使用异步方式时,发送任务到执行器的方法立即返回,所以任务能够继续执行。
你应该意识到这两种方法之间的巨大差异。 当使用同步方法时,调用这些方法之一(例如invokeAll()方法)的任务将暂停直到发送到线程池的任务结束执行,这允许ForkJoinPool类使用工作窃取算法来分配新的任务到执行休眠任务的工作线程。与之相反,当使用异步方法(例如fork()方法)时,任务将继续执行,因此ForkJoinPool类无法使用工作窃取算法来提升应用性能。这种情况下,只有调用join()或者get()方法来等待任务结束,ForkJoinPool类才能使用工作窃取算法。
除了RecursiveAction和RecursiveTask类,Java 8 引入新的支持CountedCompleter类的ForkJoinTask类,在这种任务中,当任务被加载并且没有待定子任务时,能够包含一个完成操作。此机制基于包含在类中的方法(onCompletion()方法)和待定任务的计数器。
计数器初始化默认为零,当需要在原子方式时可以递增它。通常地,当加载一个子任务时,逐次递增计数器。租后,当任务结束执行时,尝试完成任务执行并因此执行onCompletion()方法。如果待定数大于零,计数器加一。如果计数器为零,执行onCompletion()方法,然后完成父任务。
在本节中,学习使用ForkJoinPool和CountedCompleter类提供的异步方法来管理任务,实现用来在指定文件夹及子文件夹中寻找文件的程序。实现的CountedCompleter类用来处理文件夹目录,对文件夹中的每个子文件夹,以异步方式发送一个新的任务到ForkJoinPool类。对文件夹下的每个文件,如果任务继续执行,将检查文件的后缀名并将文件添加到结果列表中。当任务完成时,所有子任务的结果列表将插入到结果任务中。
实现过程
通过如下步骤实现范例:
-
创建名为FolderProcessor的类,继承List参数化的CountedCompleter类:
public class FolderProcessor extends CountedCompleter<List<String>>{
-
定义名为path的私有String属性,用来存储准备处理任务的文件夹完整路径:
private String path;
-
定义名为extension的私有String属性,用来存储准备检索任务的文件后缀名:
private String extension;
-
定义两个名为tasks和resultList的私有List属性,第一个用来存储任务加载的所有子任务,第二个用来存储任务的结果列表:
private List<FolderProcessor> tasks; private List<String> resultList;
-
实现类构造函数,初始化属性和父类。因为只在内部使用,所以定义此构造函数为protected类型:
public FolderProcessor(CountedCompleter<?> completer, String path, String extension) { super(completer); this.path = path; this.extension = extension; }
-
实现外部使用的公共构造函数,由于此构造函数创建的任务没有父任务,所以参数中不需要此对象:
public FolderProcessor(String path, String extension) { this.path = path; this.extension = extension; }
-
实现compute()方法,由于CountedCompleter类是任务的基类,所以此方法返回类型是void:
@Override public void compute() {
-
首先,初始化两个列表属性:
resultList = new ArrayList<>(); tasks = new ArrayList<>();
-
获得文件夹目录:
File file = new File(path); File content[] = file.listFiles();
-
对文件夹的每个元素,如果有子文件夹,则创建新的FolderProcessor对象,使用fork()方法异步执行此对象。这里用到第一个类构造函数并且将当前任务作为新的完整任务传递,以及使用addToPendingCount()方法增加待定任务计数器的值:
if (content != null) { for (int i = 0; i < content.length; i++) { if (content[i].isDirectory()) { FolderProcessor task=new FolderProcessor(this, content[i].getAbsolutePath(), extension); task.fork(); addToPendingCount(1); tasks.add(task);
-
否则,使用checkFile()方法比较文件与检索文件的后缀名,如果相同,存储文件完整路径到先前定义的字符串列表中:
}else{ if (checkFile(content[i].getName())){ resultList.add(content[i].getAbsolutePath()); } } }
-
如果FolderProcessor子任务列表超过50个元素,输出指明此情况的信息到控制台:
if (tasks.size()>50) { System.out.printf("%s: %d tasks ran.\n", file.getAbsolutePath(),tasks.size()); } }
-
最后,使用tryComplete()方法尝试完成当前任务:
tryComplete(); }
-
实现onCompletion()方法,此方法将在所有子任务(从当前任务分支出的所有任务)已经完成运行时执行。将所有子任务的结果列表添加到当前任务的结果列表中:
@Override public void onCompletion(CountedCompleter<?> completer) { for (FolderProcessor childTask : tasks) { resultList.addAll(childTask.getResultList()); } }
-
实现checkFile()方法,此方法比较作为参数传进来的的文件是否与检索文件的后缀名相同,如果是,此方法返回true值,否则返回false值:
private boolean checkFile(String name) { return name.endsWith(extension); }
-
最后,实现getResultList()方法返回任务的结果列表,代码很简单,不在此列出。
-
实现范例的主方法,创建一个包含main()方法的Main类:
public class Main { public static void main(String[] args) {
-
使用默认构造函数创建ForkJoinPool:
ForkJoinPool pool = new ForkJoinPool();
-
创建三个FolderProcessor任务,分别初始化不同的文件夹路径:
String prefix = "log"; FolderProcessor system = new FolderProcessor("C:\\Windows", prefix); FolderProcessor apps = new FolderProcessor("C:\\Program Files", prefix); FolderProcessor documents = new FolderProcessor("C:\\Documents And Settings", prefix);
-
使用execute()方法在线程池中执行这三个任务:
pool.execute(system); pool.execute(apps); pool.execute(documents);
-
每隔1秒输出线程池状态信息到控制台,直到这三个任务结束执行:
do { System.out.printf("******************************************\n"); 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(); System.out.printf("Documents: %d files found.\n", results.size()); }
工作原理
下图显示本范例在控制台输出的部分执行信息:
FolderProcessor类是此范例的关键之处,每个任务处理一个文件夹的目录,目录包括如下两种元素:
- 文件
- 其它文件夹
如果任务遍历到一个文件夹,则创建另一个FolderProcessor对象来处理这个文件,并且使用fork()方法将此对象发送到线程池中,此方法将任务发送到线程池中,如果池中有空闲的工作线程则执行任务,或者创建一个新的线程。此方法立即返回,所以任务能够继续处理文件夹目录。对每个文件,一个任务用来比较其后缀名和检索文件后缀名,如果相同,将文件名添加到results列表中。
一旦任务处理完指派文件夹下的所有目录,尝试完成当前任务。在本节的介绍中解释到,当我们尝试完成任务时,CountedCompleter源码检索待定任务计数器的值,如果大于0,则减少计数器的值。与之相反,如果值等于0,任务执行onCompletion()方法,然后尝试完成父任务。本范例中,当任务处理文件夹且找到子文件夹时,创建一个新的子任务,使用fork()方法加载此任务,且增加待定任务的计数器值。所以当任务已经处理所有目录时,待定任务的计数器值将与加载的子任务数相同。当调用tryComplete()方法时,如果当前任务的文件夹有子文件夹,这个调用将减少待定任务的计数器值。只有当任务的所有子任务已经完成时,才执行其onCompletion()方法。如果当前任务的文件夹中没有任何子文件夹,待定任务的计数器值将为零,onComplete()方法会被立即调用,然后将尝试完成父任务。通过这种方式,我们创建了一个从头到尾的任务树,这些任务从尾到头完成。在onComplete()方法中,我们处理子任务的所有结果列表,并将这些元素添加到当前任务的结果列表中。
ForkJoinPool类也允许任务以异步方式执行。通过使用execute()方法发送三个初始任务到线程池。在Main类中,使用shutdown()方法结束池操作,且输出正在池中运行的任务状态和进展信息。针对异步方式,ForkJoinPool类还包括很多有用的方法。学习第九章“测试并发应用”中的“监控fork/join池”小节,了解这些方法的完整列表。
扩展学习
本范例中用到addToPendingCount()方法增加待定任务的计数器值,我们也可以使用其它方法来改变这个值:
- setPendingCount():此方法给待定任务计数器赋值。
- compareAndSetPendingCount():此方法接收两个参数,第一个是预期值,第二个是新的数值。如果待定任务计数器值等于预期值,则将计数器值设置成第二个值。
- decrementPendingCountUnlessZero():此方法减少待定任务计数器的值,直到等于零。
CountedCompleter类也包含其它方法来管理任务的完成,如下是最重要的两个方法:
- complete():此方法独立于待定任务计数器的值来执行onCompletion()方法,并且尝试完成其完整(父)任务。
- onExceptionalCompletion():当completeExceptionally()已经被调用或者compute()方法已经跑出一个Exception时,调用此方法。用处理这种异常的代码来重写此方法。
本范例中,用到join()方法等待任务的结束且获得结果,也可以用如下的get()方法达到这个目的:
- get(long timeout, TimeUnit unit):在这个方法中,如果任务的结果无效,则等待指定的时间,如果已过指定时间且结果依然无效,此方法返回null值。TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、和SECONDS。
- join()方法无法被中断,如果中断调用join()方法的线程,此方法会跑出InterruptedException异常。
更多关注
- 本章“创建fork/join池”小节
- 第九章“测试并发应用”中的“监控fork/join池”小节。