11.5.5 任务执行

线程最基本的用法是在程序的主线程之外执行其他的任务。最简单的做法是创建一个表示线程的Thread类的对象,再调用start方法启动该线程的运行。这种做法虽然简单,但要求开发人员对创建出来的线程进行维护,当线程较多时,会带来不小的维护成本。在线程较多时,一般的做法是创建一个线程池来进行统一管理,同时降低重复创建线程的开销。在Java早期版本中,开发人员需要自己开发线程池的实现,或者利用第三方库。在J2SE 5.0中,Java标准库的java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。

首先介绍在执行任务时会用到的几个比较重要的接口。在J2SE 5.0之前,Runnable接口用来表示一个可执行的任务。不过Runnable接口有一些局限性,主要是受限于run方法的类型签名。这些局限性通过Callable接口来解决。Callable接口只有一个方法call。这个call方法可以有返回值,同时可以抛出受检异常。这两点是Callable接口相对于Runnable接口的优势。不过Callable接口的实现对象不能通过直接创建Thread类的对象的方式来执行,而需要使用java.util.concurrent包提供的任务执行API。

当需要异步执行一个任务时,一般的做法是把任务的执行封装在一个线程中。如果需要获取任务的执行结果,则要求在主线程和任务执行线程之间进行同步和数据传递。Future接口简化了任务的异步执行。Future接口可以作为异步操作的一个抽象。调用Future接口的get方法可以获取异步任务的执行结果。如果任务没有执行完,那么调用get方法的线程会处于等待状态,直到任务完成或被取消。如果希望取消一个任务的执行,那么可以调用cancel方法。

有些任务对执行时的调度方式有一定的要求,这些任务不在提交之后就立即执行,而是需要等待一段时间。Delayed接口用来声明任务在调度方式上的这种需求。Delayed接口中的getDelay方法用来返回当前剩余的延迟时间。当getDelay方法的返回值不大于0时,说明所延迟的时间已经过去,应该调度并执行该任务。

把Runnable、Future和Delayed接口组合起来,可以形成具备组合功能的新接口。RunnableFuture接口继承自Runnable接口和Future接口。当来自Runnable接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经成功完成,可以通过get方法来获取运行的结果。ScheduledFuture接口继承自Delayed接口和Future接口,表示一个可以进行调度的异步操作。RunnableScheduledFuture接口则同时继承自Runnable、Delayed和Future接口。RunnableScheduledFuture接口的实现对象是可调度的,同时通过run方法的成功运行来表示异步操作的完成。RunnableScheduledFuture接口中包含的方法isPeriodic用来表明该异步操作是否可以被重复调度执行。

上面介绍的接口都是用来描述任务本身的,与任务的执行没有关系。在执行任务时,可以手动创建Thread类的对象,不过更好的做法是使用java.util.concurrent包中提供的与任务执行相关的接口和实现。最基本的接口是Executor,其中的execute方法用来执行一个Runnable接口的实现对象。不同的Executor接口实现可能采取不同的任务执行策略。Executor接口所提供的任务执行功能比较弱,只能处理Runnable接口。ExecutorService接口继承自Executor接口,并提供了更多实用的功能,其中,第一项重要功能是对任务的管理。使用ExecutorService接口的submit方法可以把Callable接口和Runnable接口的实现对象作为任务来提交,得到一个Future接口的实现对象作为返回值。通过该Future接口的实现对象可以获取任务的执行结果或取消任务。第二项功能是任务的批量执行。通过invokeAll和invokeAny方法可以同时提交多个Callable接口的实现对象。调用invokeAll方法之后,会等待所有的任务都执行完成,返回值是一个包含每个任务对应的Future接口实现对象的列表,从中可以获取每个任务的运行结果。调用invokeAny方法之后,任何一个任务成功完成后,都会把该任务的执行结果返回给调用者。最后一项功能是任务执行服务的关闭。当不再需要使用ExecutorService接口的实现对象时,可以调用shutdown或shutdownNow方法来关闭服务。两者的区别在于,shutdown方法只是不再允许新的任务被提交,在shutdown方法被调用之前提交的任务仍然可以继续运行;而shutdownNow方法会试图终止正在运行的和等待运行的任务,并返回已经提交但没有被运行的任务的列表。这两个方法都不会等待服务真正关闭,只是发出关闭请求。通过调用ExecutorService接口的awaitTermination方法可以使当前线程在一定时间内等待服务完成关闭。在shutdownNow方法中强制终止任务时,通常的做法是向线程发出中断请求,因此确保提交的任务实现了正确的中断处理逻辑,能够在收到中断请求时进行必要的清理工作并结束任务。

如果需要对任务的执行方式进行调度,可以使用继承自ExecutorService接口的ScheduledExecutorService接口。ScheduledExecutorService接口支持任务的延迟执行和定期执行。可以执行的任务由Callable或Runnable接口来表示。通过schedule方法可以调度一个任务在延迟若干时间之后再执行;而scheduleAtFixedRate方法则调度一个任务在初始的延迟时间后,每隔一段时间重复执行,在下一次执行开始时,上一次执行可能还没有结束;scheduleWithFixedDelay方法与scheduleAtFixedRate方法的作用类似,只不过scheduleWithFixedDelay方法是在上一次任务执行完成之后,经过给定的间隔时间再开始下一次的执行。所以scheduleAtFixedRate方法可能造成任务执行时的重叠,而scheduleWithFixedDelay方法则不会。这三个方法的返回值都是ScheduledFuture接口的实现对象。

在java.util.concurrent包中提供了这些任务执行接口的默认实现。在大多数情况下,使用默认实现已经足够好。这些默认实现也提供了比较多的配置项,允许开发人员进行自定义。通过Executors类的静态工厂方法可以创建出ExecutorService和ScheduledExecutorService接口的实现对象,比如调用newFixedThreadPool方法可以创建出一个使用固定数量的线程池来执行任务的ExecutorService接口的实现对象。

代码清单11-19给出了一个利用ExecutorService接口的使用多线程方式下载文件的Java类FileDownloader。ExecutorService接口的实现对象使用了10个线程来处理下载请求。在进行文件下载时,通过submit方法提交一个新的Callable接口的实现对象到ExecutorService中。当不再使用FileDownloader类的对象时,应该使用close方法来关闭其中包含的ExecutorService接口的实现对象,否则,虚拟机不会退出,所占用的内存也不会释放。在close方法的实现中,首先使用shutdown方法来发出关闭请求,此时新的任务不会被接受。接着使用awaitTermination方法来等待一段时间,使正在执行和等待执行的任务有机会可以完成。如果这些任务超过给定的时间仍没有完成,那么使用shutdownNow方法来强制结束。在调用shutdownNow方法之后,再使用awaitTermination方法来等待另外一段时间,使被强制结束的任务可以完成必要的清理工作。

代码清单11-19 ExecutorService接口的使用示例


public class FileDownloader{

private final ExecutorService executor=Executors.newFixedThreadPool(10);

public boolean download(final URL url, final Path path){

Future<Path>future=executor.submit(new Callable<Path>(){

public Path call(){

try{

InputStream is=url.openStream();

Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);

return path;

}catch(IOException e){

return null;

}

}

});

try{

return future.get()!=null?true:false;

}catch(InterruptedException|ExecutionException e){

return false;

}

}

public void close(){

executor.shutdown();

try{

if(!executor.awaitTermination(3,TimeUnit.MINUTES)){

executor.shutdownNow();

executor.awaitTermination(1,TimeUnit.MINUTES);

}

}catch(InterruptedException e){

executor.shutdownNow();

Thread.currentThread().interrupt();

}

}

}


在使用ExecutorService接口时,通过submit方法提交任务,并得到一个Future接口的实现对象来获取任务的执行结果。在有些情况下,任务的提交者和任务执行结果的使用者是程序中的不同部分。如果使用ExecutorService接口,就需要把得到的Future接口的对象在不同部分之间进行传递。更好的做法是使用CompletionService接口。程序中的不同部分可以共享CompletionService接口的实现对象。任务的提交者通过submit方法提交表示任务的Runnable和Callable接口的实现对象,而任务执行结果的使用者可以通过take或poll方法获取表示执行结果的Future接口的实现对象。两个方法的区别在于take是阻塞式的,而poll是非阻塞式的。ExecutorCompletionService类是Java标准库提供的CompletionService接口的实现类。在创建ExecutorCompletionService类的对象时需要提供一个Executor接口的实现对象作为参数,用来进行实际的任务执行。