在初识Future一文中介绍了Future的核心方法。本文中接着介绍如何用Future优化我们的程序性能。 在此之前,有必要介绍Future接口的一个实现类FutureTask。
FutureTask介绍
FutureTask继承结构
首先我们看一下FutureTask的继承结构:
public class FutureTask<V> implements RunnableFuture<V>{
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可以看出,FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future,也就是说FutureTask既是Runnable,也是Future。所以 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
下面的例子展示了FutureTask的 简单 用法 :
class FutureTaskCallable implements Callable{
@Override
public String call() throws Exception {
return "Hello FutureTask";
}
}
public class FutureTaskExample1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask =
new FutureTask<>(new FutureTaskCallable());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("子线程返回值:" + futureTask.get());
}
}
//输出
子线程返回值:Hello FutureTask
这段程序中,可以看出FutureTask类的构造函数是Callable类型;在创建了FutureTask对象后,然后将这个对象当作一个 Runnable 放到 new Thread() 中去执行,最后再用 FutureTask 的 get 得到子线程 的 返回结果。简单介绍完FutureTask,现在可以进入文章的主题了:如何利用Future优化我们的程序性能。
利用Future优化程序
考虑这样一个场景,小码哥突然心血来潮想练练字了,但是他现在手头连一只钢笔都没有,书桌也是乱糟糟的,于是他打算在网购一支钢笔,然后整理书桌,就开始练字,用代码实现这个场景
普通多线程版本
我们先来看普通的多线程版本是如何实现。
//购买钢笔任务
class BuyPenRunnable implements Runnable{
private String pen;
@Override
public void run() {
System.out.println("【购买钢笔】:下单,等待送货上门");
//模拟送货时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【购买钢笔】:快递送到");
pen = "神笔";
}
public String getPen() {
return pen;
}
}
public class CommomThreadExample {
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
BuyPenRunnable buyPenRunnable = new BuyPenRunnable();
Thread buyPenThread = new Thread(buyPenRunnable);
buyPenThread.start();
//模拟整理书桌耗时3000ms
System.out.println("【整理书桌】:开始整理");
Thread.sleep(3000);
System.out.println("【整理书桌】:整理完了");
//保证笔送到
buyPenThread.join();
//所有准备好,开始练字
write(buyPenRunnable.getPen());
long endTime = System.currentTimeMillis();
System.out.println("总共用时 " + (endTime - startTime) + " ms");
}
private static void write(String pen){
System.out.println("【开始写字】:" + pen);
}
}
//输出
【购买钢笔】:下单,等待送货上门
【购买钢笔】:快递送到
【整理书桌】:开始整理
【整理书桌】:整理完了
【开始写字】:神笔
总共用时 5020 ms
这里尽管将购买钢笔和整理桌子并行化了,节省了时间,但是这个版本有2个问题:
-
通过 Runnable 实现任务,但为了获取结果,我们需要定义共享变量 pen,并提供一个方法 getPen() 来获取这个变量。
-
任务的取消和状态检查不方便。
Future实现
利用FutureTask实现上面的功能,代码如下:
//购买钢笔任务
class BuyPenCallable implements Callable {
@Override
public String call() {
System.out.println("【购买钢笔】:下单,等待送货上门");
//模拟送货时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【购买钢笔】:快递送到");
return "神笔";
}
}
public class FutureTaskExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
Callable<String> buyPenCallable = () -> {
System.out.println("【购买钢笔】:下单,等待送货上门");
// 模拟送货时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【购买钢笔】:快递送到");
return "神笔";
};
FutureTask<String> futureTask = new FutureTask<>(buyPenCallable);
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(futureTask);
// 模拟整理书桌耗时3000ms
System.out.println("【整理书桌】:开始整理");
Thread.sleep(3000);
System.out.println("【整理书桌】:整理完了");
// 尝试取消任务,如果需要这么做
//boolean cancelled = future.cancel(false);
// 等待获取笔的结果
String pen = futureTask.get();
// 所有准备好,开始练字
write(pen);
long endTime = System.currentTimeMillis();
System.out.println("总共用时 " + (endTime - startTime) + " ms");
executor.shutdown();
}
private static void write(String pen) {
System.out.println("【开始写字】:" + pen);
}
}
上面的代码中,可以看到,FutureTask实现的实现方式:
-
获取结果更方便:通过 Callable 的 call() 方法直接返回结果,不需要显式地定义共享变量和同步机制。
-
对线程的管理更方便:FutureTask 提供了 cancel() 方法,可以取消任务,并通过 isCancelled() 和 isDone() 方法检查任务状态。
总结
在我们的实际项目中,很多时候我们的接口需要通过rpc调用去调用其它的多个服务拿到结果聚合,如果采用同步调用的方式,假设一次远程调用的时间为 300ms,则一个 Client 同步对三个 Server 分别进行一次 RPC 调 用的总时间,需要耗费 900ms。
如果使用 Future 模式对其进行改造,将同步的 RPC 调用改为异步并发的 RPC 调用,一个 Client 异步并发对三个 Server 分别进行一次 RPC 调用,那么正常情况下,大约只需要 300ms就能拿到所有服务的数据了。Future的好处不言而喻。