本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
Future,未来,未来无限可能,用这句话作为开场白吧。 这是并发编程的第四篇文章了。感谢阅读,并希望以后持续关注,我会输出更多技术干货,我们共同进步!
以后可能会分为几大专题,类似于并发专题,源码专题,面试专题等(只会分享干货)。
请出主角:Future
Q: Future是什么,有什么作用?
-
Future 最主要的作用是,比如当做一定运算的时候,运算过程可能比较耗时,有时会去查数据库,或是繁重的计算,比如压缩、加密等,在这种情况下,如果我们一直在原地等待方法返回,显然是不明智的,整体程序的运行效率会大大降低。我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。这样一来就可以把整个程序的运行效率提高,是一种异步的思想。
-
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作
-
举个例子:比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟。那Future这种模式就是你在等包子的时候,可以同时准备凉菜,所以在准备凉菜的过程中,可以同时准备包子,这样只需要等待3分钟。
Q:为什么不用多线程,照样可以异步
- 在并发编程中,我们经常用到非阻塞的模型,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。
讲Future必须先提Callable
上面我们已经提到了,有两种创建线程的方法:一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。
Callable 是一个类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 接口的类都是可以被其他线程执行的任务。 我们看一下 Callable 的源码:
public interface Callable<V> {
V call() throws Exception;
}
可以看出它也是一个 interface,并且它的 call 方法中已经声明了 throws Exception,前面还有一个 V 泛型的返回值,实现 Callable 接口,就要实现 call 方法,这个方法的返回值是泛型 V,如果把 call 中计算得到的结果放到这个对象中,就可以利用 call 方法的返回值来获得子线程的执行结果了。
我们看一下 Runnable 的源码
public interface Runnable {
public abstract void run();
}
明显,返回值是void。
Callable 和 Future 的关系
前面讲过,Callable 接口相比于 Runnable 的一大优势是可以有返回结果,那这个返回结果怎么获取呢?就可以用 Future 类的 get 方法来获取 。因此,Future 相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过 Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之 Future 的功能比较丰富。有了这样一个从宏观上的概念之后,我们就来具体看一下 Future 类的主要方法。
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, TimeoutExceptio
}
其中,第 5 个方法是对第 4 个方法的重载,方法名一样,但是参数不一样。
get() 方法:获取结果
- get 方法最主要的作用就是获取任务执行的结果,该方法在执行时的行为取决于 Callable 任务的状态。
下面用图的形式让过程更清晰:
在图中,右侧是一个线程池,线程池中有一些线程来执行任务。重点在图的左侧,可以看到有一个 submit 方法,该方法往线程池中提交了一个 Task,这个 Task 实现了 Callable 接口,当我们去给线程池提交这个任务的时候,调用 submit 方法会立刻返回一个 Future 类型的对象,这个对象目前内容是空的,其中还不包含计算结果,因为此时计算还没有完成。
当计算一旦完成时,也就是当我们可以获取结果的时候,线程池便会把这个结果填入到之前返回的 Future 中去(也就是 f 对象),而不是在此时新建一个新的 Future。这时就可以利用 Future 的 get 方法来获取到任务的执行结果了。
代码示例
我们拿上面那个吃早点的例子演示代码:
需求场景:等早餐过程中,包子需要3秒,凉菜需要1秒,普通的多线程需要四秒才能完成。先等凉菜,再等包子,因为等凉菜时,普通多线程启动start()方法,执行run()中具体方法时,没有返回结果,所以如果要等有返回结果,必须是要1秒结束后才知道结果。
普通多线程
public class BumThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000*3);
System.out.println("包子准备完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ColdDishThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("凉菜准备完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
// 等凉菜 -- 必须要等待返回的结果,所以要调用join方法
Thread t1 = new ColdDishThread();
t1.start();
t1.join();
// 等包子 -- 必须要等待返回的结果,所以要调用join方法
Thread t2 = new BumThread();
t2.start();
t2.join();
long end = System.currentTimeMillis();
System.out.println("准备完毕时间:"+(end-start));
}
采用Future模式
public static void main(String[] args) throws InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
// 等凉菜
Callable ca1 = new Callable(){
@Override
public String call() throws Exception {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "凉菜准备完毕";
}
};
FutureTask<String> ft1 = new FutureTask<String>(ca1);
new Thread(ft1).start();
// 等包子 -- 必须要等待返回的结果,所以要调用join方法
Callable ca2 = new Callable(){
@Override
public Object call() throws Exception {
try {
Thread.sleep(1000*3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "包子准备完毕";
}
};
FutureTask<String> ft2 = new FutureTask<String>(ca2);
new Thread(ft2).start();
System.out.println(ft1.get());
System.out.println(ft2.get());
long end = System.currentTimeMillis();
System.out.println("准备完毕时间:"+(end-start));
}
显而易见,Future模式更胜一筹!
还没懂,还没看够代码示例,那就再来一组
针对每种方法函数展示代码(深入剖析):
/**
* 描述 :演示一个 经典Future 的使用方法
*/
public class OneFuture {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
Future<Integer> future = service.submit(new CallableTask());
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
service.shutdown();
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return new Random().nextInt();
}
}
}
在这段代码中,main 方法新建了一个 10 个线程的线程池,并且用 submit 方法把一个任务提交进去。这个任务如代码的最下方所示,它实现了 Callable 接口,它所做的内容就是先休眠三秒钟,然后返回一个随机数。接下来我们就直接把 future.get 结果打印出来,其结果是正常打印出一个随机数,比如 100192 等。这段代码对应了我们刚才那个图示的讲解,这也是 Future 最常用的一种用法。
isDone() 方法:判断是否执行完毕:
下面我们再接着看看 Future 的一些其他方法,比如说 isDone() 方法,该方法是用来判断当前这个任务是否执行完毕了。
需要注意的是,这个方法如果返回 true 则代表执行完成了;如果返回 false 则代表还没完成。但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。
我们用一个代码示例来看一看,代码如下所示:
public class GetException {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(20);
Future<Integer> future = service.submit(new CallableTask());
try {
for (int i = 0; i < 5; i++) {
System.out.println(i);
Thread.sleep(500);
}
System.out.println(future.isDone());
future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
throw new IllegalArgumentException("Callable抛出异常");
}
}
}
在这段代码中,可以看到有一个线程池,并且往线程池中去提交任务,这个任务会直接抛出一个异常。那么接下来我们就用一个 for 循环去休眠,同时让它慢慢打印出 0 ~ 4 这 5 个数字,这样做的目的是起到了一定的延迟作用。在这个执行完毕之后,再去调用 isDone() 方法,并且把这个结果打印出来,然后再去调用 future.get()。
这段代码的执行结果是这样的:
0
1
2
3
4
true
java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Callable抛出异常
...
这里要注意,我们知道这个异常实际上是在任务刚被执行的时候就抛出了,因为我们的计算任务中是没有其他逻辑的,只有抛出异常。我们再来看,控制台是什么时候打印出异常的呢?它是在 true 打印完毕后才打印出异常信息的,也就是说,在调用 get 方法时打印出的异常。
这段代码证明了三件事情:第一件事情,即便任务抛出异常,isDone 方法依然会返回 true;第二件事情,虽然抛出的异常是 IllegalArgumentException,但是对于 get 而言,它抛出的异常依然是 ExecutionException;第三个事情,虽然在任务执行一开始时就抛出了异常,但是真正要等到我们执行 get 的时候,才看到了异常。
isCancelled() 方法:判断是否被取消
最后一个方法是 isCancelled 方法,判断是否被取消,它和 cancel 方法配合使用,比较简单。
用 FutureTask 来创建 Future
上面早点当中的例子已经很鲜明了,这里再来点代码帮助大家更深入下:
除了用线程池的 submit 方法会返回一个 future 对象之外,同样还可以用 FutureTask 来获取 Future 类和任务的结果。
FutureTask 首先是一个任务(Task),然后具有 Future 接口的语义,因为它可以在将来(Future)得到执行的结果。
我们来看一下 FutureTask 的代码实现:
public class FutureTask<V> implements RunnableFuture<V>{
...
}
可以看到,它实现了一个接口,这个接口叫作 RunnableFuture。我们再来看一下 RunnableFuture 接口的代码实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可以看出,它是 extends Runnable 和 Future 这两个接口的,它们的关系如下图所示:
既然 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 又实现了 RunnableFuture 接口,所以 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。
下面我们就用代码来演示一下:
/**
* 描述: 演示 FutureTask 的用法
*/
public class FutureTaskDemo {
public static void main(String[] args) {
Task task = new Task();
FutureTask<Integer> integerFutureTask = new FutureTask<>(task);
new Thread(integerFutureTask).start();
try {
System.out.println("task运行结果:"+integerFutureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程正在计算");
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
在这段代码中可以看出,首先创建了一个实现了 Callable 接口的 Task,然后把这个 Task 实例传入到 FutureTask 的构造函数中去,创建了一个 FutureTask 实例,并且把这个实例当作一个 Runnable 放到 new Thread() 中去执行,最后再用 FutureTask 的 get 得到结果,并打印出来。
执行结果是 4950,正是任务里 0+1+2+...+99 的结果。
总结
今天关于Future就讲到这里,大家应该都已经get到了,感谢你能阅读到这里,希望你收获满满。我们下期见。
欢迎大家点赞,关注。我会持续输出更多技术干货!
弦外之音
学习一点点,努力一点点,分享一点点,大家加油!