Future和Callable
Callable接口
Callable接口是对Runnable接口的一种补充
为什么这么说呢,因为我们之前调用Runnable接口时就发现,其中的run()方法是没有返回值的,同时也不能抛出异常。
public abstract void run();
因为所以我们都是在run()方法内部处理异常的。
而Callable接口则正是应对这两种情况的接口。
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
可以看到,Callable接口的call方法不仅提供了泛型返回值,同时能够向上抛出异常。
Future接口
Future接口是用来表示异步计算的结果。
Future的主要方法:
- get() 返回计算结果
- get(long timeout, TimeUnit unit) 在给定时间内完成则返回计算结果
- cancel(boolean mayInterruptIfRunning) 是否中断正在执行中的任务
- isCancelled() 如果正常完成之前被取消,返回true
- isDone() 任务执行完毕返回true
get()的几种情况:
- 任务已完成,返回结果
- 任务未开始或未完成,阻塞等待结果
- call方法产生异常,get()抛出ExecutionException
- 任务被取消,get()抛出CancellationException
- get(long timeout, TimeUnit unit)任务超时,抛出TimeoutException
线程池submit()获取future对象
提交任务给线程池,调用submit()方法后,会立即返回一个空的Future容器,等待任务执行完成后,再向容器填充计算结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
Future<Integer> future = threadPool.submit(() -> {
TimeUnit.SECONDS.sleep(2);
return new Random().nextInt();
});
System.out.println(future.get());
threadPool.shutdown();
}
运行后可以看到调用get方法会阻塞2秒,等待submit执行完毕返回结果后,get方法才生效。
使用集合存储多个Future对象
同样,当有多个计算结果返回时,我们可以使用集合存储多个Future对象来存储返回结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ArrayList<Future> futures = new ArrayList<>(10);
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i <10; i++) {
Future<Integer> future = threadPool.submit(() -> {
TimeUnit.SECONDS.sleep(2);
return new Random().nextInt();
});
futures.add(future);
}
for (Future future : futures) {
System.out.println(future.get());
}
threadPool.shutdown();
}
这里用ArrayList存储Future对象,并且由于线程池只有两个线程,所以每次get方法调用时,会等待2秒后输出两个整型,然后继续阻塞。
call方法发生异常时
这段代码逻辑是:判断异常会被哪个catch代码块捕获住,同时使用打点计数,验证当调用get方法时才会捕获到异常,同样也验证了isDone()方法并不仅仅是任务正常完成会返回true,任务异常也会返回true。
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Future<Integer> future = threadPool.submit(() -> {
throw new RuntimeException("Callable发生异常");
});
try {
for (int i = 0; i <5; i++) {
System.out.println(i+1);
TimeUnit.SECONDS.sleep(1);
}
System.out.println(future.isDone());
future.get();
} catch (InterruptedException e) {
System.out.println("InterruptedException");
} catch (ExecutionException e) {
System.out.println("ExecutionException");
}
threadPool.shutdown();
}
1
2
3
4
5
true
ExecutionException
get()、cancel()超时的情况
下面就演示一下超时的get()方法和cancel方法的使用
这段代码逻辑:任务:休眠2秒,休眠完成后返回字符串
调用不同超时时间的get方法进行判断,最后输出的字符串来自哪行命令。
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
Future<String> future = threadPool.submit(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
System.out.println("sleep被中断了");
return "sleep被中断后返回的String";
}
return "2秒后正常运行结束";
});
String s = null;
try {
s = future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.out.println("get被中断了");
s="get被中断后返回的String";
} catch (ExecutionException e) {
System.out.println("内部发生异常了");
s="call方法内部出现异常时的String";
} catch (TimeoutException e) {
System.out.println("超时异常了");
s="发生超时异常时的String";
}
System.out.println(s);
threadPool.shutdown();
}
2秒后正常运行结束
首先是get(10秒),没超时的情况下。10秒内,任务结束返回字符串,get方法就将该值赋值给s,最后输出。
再来看看如果get()超时时间为1秒
s = future.get(1, TimeUnit.SECONDS);
超时异常了
发生超时异常时的String
会跳转到catch (TimeoutException e)
代码块
那么我们在这个代码块中,调用cancel方法会如何。
catch (TimeoutException e) {
System.out.println("超时异常了");
s="发生超时异常时的String";
boolean cancel = future.cancel(false);
System.out.println(cancel);
}
cancel方法会传入一个boolean类型,判断是否中断当前任务。
先传入false。
按照逻辑,超时跳转到这里时,先输出一句话,然后s赋值,我们调用cancel方法,输出中断任务是否成功。
超时异常了
true
发生超时异常时的String
可以看到,都是按照预期输出的。
那么如果中断当前任务,会进入这个catch代码块
catch (InterruptedException e) {
System.out.println("sleep被中断了");
return "sleep被中断后返回的String";
}
那么返回结果会不会给s赋值?
超时异常了
sleep被中断了
true
发生超时异常时的String
答案是不会,超时之后,任务的return全都失效了。
即使是发生了中断异常,执行了代码,但是不会将结果return回来。
就像老师说收个作业,这周之内发到他的邮箱,否则没有成绩。如果下周发过去,已经超时了,获取不到这个作业的成绩了。
cancel()方法也有以下几种情况:
- 任务还没启动,调用cancel(),直接取消不启动,返回true
- 任务已完成,返回false
- 任务正在运行中:根据传入参数判断是否中断线程
通常我们在确定任务能正确响应interrupt的时候,才会选择传入true
FutureTask
通过Callable来获取Future对象是有一定限制的,因为只能通过线程池的submit()方法来获取,无法直接交给一个线程或者Executor实现类来执行。
所以JUC又提供了一个类:FutureTask
这个类实现了RunnableFuture接口,而该接口同时实现了Future接口也实现了Runnable接口
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
所以它既可以被当做任务执行,也可以获得callable的返回值。
通过该类的构造方法,传入一个Callable对象,生成的FutureTask对象可以作为Runnable对象传入Thread或者Executor执行。
public FutureTask(Callable<V> callable)
下面就尝试一下FutureTask的使用
public static void main(String[] args) {
Callable<String> callable = () -> {
TimeUnit.SECONDS.sleep(2);
return "FutureTask";
};
FutureTask<String> integerFutureTask = new FutureTask<>(callable);
//作为runnable传入
new Thread(integerFutureTask).start();
try {
//作为Future获取
System.out.println(integerFutureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}