一关于Future
在java中创建一个线程绝非难事,从JDK1.0开始后,调用者只需直接继承Thread类的实现,或者实现Runnable接口,便可以创建一个运行线程。但是这两种线程创建方式无法将运行结果返回给调用者,因此从JDK 1.5以后,提供了Callable与Future,是调用者可以获得任务执行完成后返回的结果。
二 Runnable 与 Callable
我们先来看一下接口Runnable与Callable在java.lang.*包下的声明(JDK8):
public interface Runnable { public abstract void run(); } public interface Callable<V> { V call() throws Exception; }
对比两个接口的声明,我们可以看到Runnable.run()没有任何返回,并且不会抛出异常,而Callable.call()接口会返回一个泛型变量V,而V的类型正是调用者传递进来的类型,在call()计算过程无法正常完成时,会抛出异常。在Executor框架下,我们可以通过ExecutorService.submit()来向线程池提交一个Callable对象,这时submit()的返回对象便是Future对象,也就是说Future对象可以获取到Callable.call的返回值,ExecutorService.submit()声明如下:
/
<T> Future<T> submit(Callable<T> task);
三Future的应用场景
关于Future的使用,我们考虑这样的实际场景,系统启动需要从远程获取系统的加载资源,假设我们通过以下函数实现需求:
private Resource getResourceFromRemote();
此时主线程会一直阻塞在getResourceFromRemote(),等待系统从远程获取到加载资源,才能继续系统的启动过程。如果该过程比较长,系统还有其他耗时的模块需要加载,并且这些模块并不依赖于远程的这些加载资源,此时系统阻塞在getResourceFromRemote(),无疑会增加系统的启动时间,是一种性能浪费。那么如何改进这一问题?最容易想到的策略就是,在系统等待远程资源的同时,同时加载其他模块,等到系统的加载后期需要用到资源时,再继续等待远程资源的获取,从而系统中相互无依赖的加载模块能并发加载,提高系统的加载速度。如下图所示,系统可以在获取远程资源的同时实现模块1和模块2的加载:
在具体实现中,我们就可以通Future与Callable将getResourceFromRemote封装成异步任务,实现如下:
private static final ExecutorService threadpool = Executors.newFixedThreadPool(3); private static Future<Resource> getResourceFromRemoteSync(){ Future<Resource> future = threadpool.submit( new Callable<Resource>() { public Resource call() throws Exception { return getResource(); } } ); return future; }
主线程只需通过Future.get()方法,便可以获取到资源获取任务的执行结果,系统的启动过程伪代码如下:
//step 1:开始获取远程资源 Future<Resource> future = getResourceFromRemoteSync(); //step 2:进行其他模块的加载工作,这里省略 //... //step 3:等待获取远程资源的任务返回的资源 Resource r = future.get();
通过这个例子,可以看出Future对象本身是一个显式的引用,一个对异步处理结果的引用,正如Future的英文的意义未来一样,Future的引用指向的是计算任务完成后产生的结果,而并非当下就需要的结果。在使用异步资源加载的系统设计中,还有一种重要的机制可以实现上述需求,回调机制。回调是一种常见的异步并发模式,它有即时性高、接口设计简单等有点。但相对于Future,其缺点也非常明显。首先,多线程环境下的回调一般是在触发回调的模块线程中执行的,这就意味着编写回调方法时通常必须考虑线程互斥问题;其次,回调方式接口的提供者在本模块的线程中执行用户应用的回调也是相对不安全的,因为你无法确定它会花费多长时间或出现什么异常,从而可能间接导致本模块的即时性和可靠性受影响;再者,使用回调接口不利于顺序流程的开发,因为回调方法的执行是孤立的,要与正常流程汇合是比较困难的。因此回调接口适合于在回调中只需要完成简单任务,并且不必与其它流程汇合的场景。
四 Future接口
在我们初步了解了Future的特性与使用场景后,我们来详细看一下Future接口的具体功能,Future声明如下(JDK 8):
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
可以看到Future提供了5个接口,get()接口我们已经用过了,表示对异步计算结果的获取,此外get()接口还重载了支持超时等待的接口,该接口如果在超时时间内未获取到计算结果,将会抛出TimeoutException异常,这样就能确保在计算任务长时间不返回时,可能造成程序僵死的情况。其他接口的功能与意义如下:
- cancel():用来取消任务的执行,起返回值表示是否成功取消任务。参数mayInterruptIfRunning的设置表示是否允许取消还未完成的任务,如果任务已经完成,那么无论mayInterruptIfRunning设置成true或false,cancel()都会返回false;如果任务正在执行,那么mayInterruptIfRunning设置为true,任务将会被取消,并且cancel()返回true,而如果mayInterruptIfRunning设置为false,任务将无法被取消,任务还将继续执行,cancel()返回false;如果任务还未执行,那么无论mayInterruptIfRunning设置为什么,任务都会被成功取消,所以cancel()也会返回true。可能这个接口的状态与参数关系比较复杂,关系如下表所示:
- 也就是说,只有cancel()方法成功取消了任务的执行,才会返回true。
- isCancelled():表示在任务执行完成之前,任务是否被取消。
- isDone():表示任务是否已经结束,无论是任务是正常结束,抛出异常还是被取消结束,都会返回true。
Future的5个接口为使用者提供了灵活的其同步或异步控制的发挥空间,开发者可以根据流程的需要自由决定是否需要等待(Future.isDone()),何时等待(Future.get())以及等待多久(Future.get(timeout))。
五 关于FutureTask
Future只是一个接口,一个接口是不能直接使用的,必须有具体类来实现,在Executor框架下实现Future接口的具体类就是FutureTask。通过ExecutorService.submit()接口提交的任务都会被封装成一个FutureTask对象,ExecutorService.submit()的实现如下:
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; } protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); }
即使调用者通过submit()接口提交的是Runnable类型的任务,在FutureTask构造函数中也会使用Executors.callable(Runnable, V)方法,将Runnable与返回结果封装成一个callable对象。
public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable }
从上面的分析,可以看出Executor框架下的Future特性的支持,是由FutureTask实现的。那么FutureTask的实现细节又如何呢?
从FutureTask类的源码可以看到FutureTask实现了RunnableFuture接口。
public class FutureTask<V> implements RunnableFuture<V> { private Callable<V> callable; public FutureTask(Callable<V> callable) { this.callable = callable; } } public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
由上,可以看到FutureTask的构造函数接受一个Callable类型的任务,并且实现了Runnable和Future接口,这样FutureTask既可以通过Thread包装来直接执行,也可以提交给ExecuteService来执行。所以FutureTask既是Future、Runnable,又是包装了的Callable(如果是Runnable最终也会被转换为Callable ), 它是这两者的合体。
总结
summary
通过本节,我们了解了Executor框架下对于Future特性的支持是由FutrueTask实现的,在实际的生产环境中使用Futrue往往能将结构复杂的代码实现整理成具有清晰流程的可读性强的代码,Futrue对我们来说更是一种JDK为我们提供的并发编程机制,使得我们队线程的执行控制力更强,更灵活。