java并发工具类-Callable、Future 和FutureTask
前言
创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果
Callable接口
Callable位于JUC包下,它也是一个接口,在它里面也只声明了一个方法叫做call():
1 |
|
Callable接口代表一段可以调用并返回结果的代码。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
Callable接口使用泛型去定义它的返回类型。
Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。
java.util.concurrent.Future
对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了**get()**方法让我们可以等待Callable结束并获取它的执行结果。
那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:
1 | <T> Future<T> submit(Callable<T> task); |
第一个方法:submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
第二个方法:submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。
第三个方法:submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
因此我们只要创建好我们的线程对象(实现Callable接口或者Runnable接口),然后通过上面3个方法提交给线程池去执行即可。
Future接口
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future
1 | public interface Future<V> { |
在Future接口中声明了5个方法,下面依次解释每个方法的作用
方法 | 作用 |
---|---|
cance(boolean mayInterruptIfRunning) | 试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false |
isCancelled() | 方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。 |
isDone() | 方法表示任务是否已经完成,若任务完成,则返回true; |
get() | 方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回; |
get(long timeout, TimeUnit unit) | 设定计算结果的返回时间,如果在规定时间内没有返回计算结果则抛出TimeOutException |
Future提供了三种功能
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。
RunnableFuture接口
RunnableFuture实现了Runnable和Future。因此FutureTask可以传递到线程对象Thread或Excutor(线程池)来执行。
1 | public interface RunnableFuture<V> extends Runnable, Future<V> { |
FutureTask
我们先来看一下FutureTask的实现:
1 | public class FutureTask<V> implements RunnableFuture<V> |
FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现
1 | public interface RunnableFuture<V> extends Runnable, Future<V> |
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
分析
FutureTask除了实现了Future接口外还实现了Runnable接口,因此FutureTask也可以直接提交给Executor执行。 当然也可以调用线程直接执行(FutureTask.run())。接下来我们根据FutureTask.run()的执行时机来分析其所处的3种状态:
未启动,FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态,当创建一个FutureTask,而且没有执行FutureTask.run()方法前,这个FutureTask也处于未启动状态。
已启动,FutureTask.run()被执行的过程中,FutureTask处于已启动状态。
已完成,FutureTask.run()方法执行完正常结束,或者被取消或者抛出异常而结束,FutureTask都处于完成状态。
下面我们再来看看FutureTask的方法执行示意图(方法和Future接口基本是一样的,这里就不过多描述了)
当FutureTask处于未启动或已启动状态时,如果此时我们执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常。
当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会执行。当FutureTask处于已启动状态时,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果任务取消成功,cancel(…)返回true;但如果执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时cancel(…)返回false。当任务已经完成,执行cancel(…)方法将返回false。
最后我们给出FutureTask的两种构造函数:
1 | public FutureTask(Callable<V> callable) { |
事实上,FutureTask是Future接口的一个唯一实现类。
使用创景
通过上面的介绍,我们对Callable,Future,RunnableFuture,FutureTask都有了比较清晰的了解了,那么它们到底有什么用呢?我们前面说过通过这样的方式去创建线程的话,最大的好处就是能够返回结果,加入有这样的场景,我们现在需要计算一个数据,而这个数据的计算比较耗时,而我们后面的程序也要用到这个数据结果,那么这个时Callable岂不是最好的选择?我们可以开设一个线程去执行计算,而主线程继续做其他事,而后面需要使用到这个数据时,我们再使用Future获取不就可以了吗?下面我们就来编写一个这样的实例。
多任务计算
利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。
例如主线程执行到需要发邮件,发短信的环节,发送完成后才能继续下一次任务,对于发短信发邮件可以使用FutureTask进行多线程进行并行执行,通过get来阻塞返回结果,如果发邮件需要5s,发短信需要8s,传统的方式需要13s,使用FutureTask异步执行只需要8s。
1 | package chapter02.future; |
高并发环境下确保任务只执行一次
在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个map的缓存,当key存在时,即直接返回key对应的对象;当key不存在时,则创创建一个对象。对于这样的应用场景,通常采用的方法为使用一个Map对象来存储key和换粗你对象的对应关系,典型的代码如下面所示:
原始的方式如下
1 | private Map<String, String> orginCacheMap = new HashMap<String, String>(); |
在上面的例子中,我们通过加锁确保高并发环境下的线程安全,也确保了缓存对象只创建一次,然而确牺牲了性能。改用ConcurrentHash的情况下,几乎可以避免加锁的操作,性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象。这时最需要解决的问题就是当key不存在时,创建缓存对象的动作能放在connectionPool之后执行,这正是FutureTask发挥作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:
1 | package chapter02.future; |
总结
实现Runnable接口和实现Callable接口的区别:
Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的。
Callable规定的方法是call(),Runnable规定的方法是run()。
Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)。
call方法可以抛出异常,run方法不可以。
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
Callable、Runnable、Future和FutureTask 的区别
Callable、Runnable、Future和FutureTask 做为java 线程池运行的重要载体,有必要深入理解。
Callable 和 Runnable 都是执行的任务的接口,区别在于Callable有返回值,而Runnable无返回值。
Future 表示异步任务返回结果的接口
RunnableFuture 继承了Runnable, Future,表示可以带有返回值的run接口
FutureTask是一个实现类,实现了RunnableFuture接口,既能接受Runnable类型的任务,也可以接受Callable类型的任务,这个类的作用主要是 有一个protected void done()方法用来扩展使用,作为一个回调方法