异步运行任务

Java 9并发编程指南 目录

异步运行任务

  • 实现过程
  • 工作原理
  • 扩展学习
  • 更多关注

在ForkJoinPool中可以使用同步或者异步的方式来执行ForkJoinTask。当使用同步方式时,发送任务到线程池中的方法直到此任务发送结束执行时才返回。当使用异步方式时,发送任务到执行器的方法立即返回,所以任务能够继续执行。

你应该意识到这两种方法之间的巨大差异。 当使用同步方法时,调用这些方法之一(例如invokeAll()方法)的任务将暂停直到发送到线程池的任务结束执行,这允许ForkJoinPool类使用工作窃取算法来分配新的任务到执行休眠任务的工作线程。与之相反,当使用异步方法(例如fork()方法)时,任务将继续执行,因此ForkJoinPool类无法使用工作窃取算法来提升应用性能。这种情况下,只有调用join()或者get()方法来等待任务结束,ForkJoinPool类才能使用工作窃取算法。

除了RecursiveAction和RecursiveTask类,Java 8 引入新的支持CountedCompleter类的ForkJoinTask类,在这种任务中,当任务被加载并且没有待定子任务时,能够包含一个完成操作。此机制基于包含在类中的方法(onCompletion()方法)和待定任务的计数器。

计数器初始化默认为零,当需要在原子方式时可以递增它。通常地,当加载一个子任务时,逐次递增计数器。租后,当任务结束执行时,尝试完成任务执行并因此执行onCompletion()方法。如果待定数大于零,计数器加一。如果计数器为零,执行onCompletion()方法,然后完成父任务。

在本节中,学习使用ForkJoinPool和CountedCompleter类提供的异步方法来管理任务,实现用来在指定文件夹及子文件夹中寻找文件的程序。实现的CountedCompleter类用来处理文件夹目录,对文件夹中的每个子文件夹,以异步方式发送一个新的任务到ForkJoinPool类。对文件夹下的每个文件,如果任务继续执行,将检查文件的后缀名并将文件添加到结果列表中。当任务完成时,所有子任务的结果列表将插入到结果任务中。

实现过程

通过如下步骤实现范例:

  1. 创建名为FolderProcessor的类,继承List参数化的CountedCompleter类:

    public class FolderProcessor extends CountedCompleter<List<String>>{
    
  2. 定义名为path的私有String属性,用来存储准备处理任务的文件夹完整路径:

    	private String path;
    
  3. 定义名为extension的私有String属性,用来存储准备检索任务的文件后缀名:

    	private String extension;
    
  4. 定义两个名为tasks和resultList的私有List属性,第一个用来存储任务加载的所有子任务,第二个用来存储任务的结果列表:

    	private List<FolderProcessor> tasks;
    	private List<String> resultList;
    
  5. 实现类构造函数,初始化属性和父类。因为只在内部使用,所以定义此构造函数为protected类型:

    	public FolderProcessor(CountedCompleter<?> completer, String path, String extension) 	 {
    		super(completer);
    		this.path = path;
    		this.extension = extension;
    	}
    
  6. 实现外部使用的公共构造函数,由于此构造函数创建的任务没有父任务,所以参数中不需要此对象:

    	public FolderProcessor(String path, String extension) {
    		this.path = path;
    		this.extension = extension;
    	}
    
  7. 实现compute()方法,由于CountedCompleter类是任务的基类,所以此方法返回类型是void:

    	@Override
    	public void compute() {
    
  8. 首先,初始化两个列表属性:

    		resultList = new ArrayList<>();
    		tasks = new ArrayList<>();
    
  9. 获得文件夹目录:

    		File file = new File(path);
    		File content[] = file.listFiles();
    
  10. 对文件夹的每个元素,如果有子文件夹,则创建新的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);
    
  11. 否则,使用checkFile()方法比较文件与检索文件的后缀名,如果相同,存储文件完整路径到先前定义的字符串列表中:

    				}else{
    					if (checkFile(content[i].getName())){
    						resultList.add(content[i].getAbsolutePath());
    					}
    				}
    			}
    
  12. 如果FolderProcessor子任务列表超过50个元素,输出指明此情况的信息到控制台:

    			if (tasks.size()>50) {
    				System.out.printf("%s: %d tasks ran.\n",
    				file.getAbsolutePath(),tasks.size());
    			}
    		}
    
  13. 最后,使用tryComplete()方法尝试完成当前任务:

    		tryComplete();
    	}
    
  14. 实现onCompletion()方法,此方法将在所有子任务(从当前任务分支出的所有任务)已经完成运行时执行。将所有子任务的结果列表添加到当前任务的结果列表中:

    	@Override
    	public void onCompletion(CountedCompleter<?> completer) {
    		for (FolderProcessor childTask : tasks) {
    			resultList.addAll(childTask.getResultList());
    		}
    	}
    
  15. 实现checkFile()方法,此方法比较作为参数传进来的的文件是否与检索文件的后缀名相同,如果是,此方法返回true值,否则返回false值:

    	private boolean checkFile(String name) {
    		return name.endsWith(extension);
    	}
    
  16. 最后,实现getResultList()方法返回任务的结果列表,代码很简单,不在此列出。

  17. 实现范例的主方法,创建一个包含main()方法的Main类:

    public class Main {
    	public static void main(String[] args) {
    
  18. 使用默认构造函数创建ForkJoinPool:

    		ForkJoinPool pool = new ForkJoinPool();
    
  19. 创建三个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);
    
  20. 使用execute()方法在线程池中执行这三个任务:

    		pool.execute(system);
    		pool.execute(apps);
    		pool.execute(documents);
    
  21. 每隔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()));	
    
  22. 使用shutdown()方法关闭ForkJoinPool:

    		pool.shutdown();
    
  23. 输出每个任务生成的结果数量到控制台:

    		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池”小节。

你可能感兴趣的:(Java,多线程)