1. 如何获取任务执行结果
ThreadPoolExecutor提供的3个submit()方法和1个FutureTask工具类来支持获得任务执行结果的需求。
3个submit()方法方法签名如下:
// 提交Runnable任务
Future<?> submit(Runnable task);
// 提交Callable任务
<T> Future<T> submit(Callable<T> task);
// 提交Runnable任务及结果引用
<T> Future<T> submit(Runnable task, T result);
上面3个方法返回Future,Future接口有5个方法,两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。
// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否已取消
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);
这3个submit()方法之间的区别在于方法参数不同,
- submit(Runnable task) :参数是一个Runnable接口,Runnable接口的run()方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的Future仅可以用来断言任务已经结束了,类似于Thread.join()。
- submit(Callable task):参数是一个Callable接口,它只有一个call()方法,并且这个方法是有返回值的,所以这个方法返回的Future对象可以通过调用其get()方法来获取任务的执行结果。
- submit(Runnable task, T result):假设这个方法返回的Future对象是f,f.get()的返回值就是传给submit()方法的参数result。注意Runnable接口的实现类Task声明了一个有参构造函数 Task(Result r) ,创建Task对象的时候传入了result对象,这样就能在类Task的run()方法中对result进行各种操作了。result相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。
ExecutorService executor = Executors.newFixedThreadPool(1);
// 创建Result对象r
Result r = new Result();
r.setAAA(a);
// 提交任务
Future<Result> future = executor.submit(new Task(r), r);
Result fr = future.get();
// 下面等式成立
fr === r;
fr.getAAA() === a;
fr.getXXX() === x
class Task implements Runnable{
Result r;
//通过构造函数传入result
Task(Result r){
this.r = r;
}
void run() {
//可以操作result
a = r.getAAA();
r.setXXX(x);
}
}
1.1 FutureTask工具类
两个构造函数:
FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);
FutureTask实现了Runnable和Future接口, 既能被ThreadPoolExecutor、Thread执行,又能获得执行结果。
// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交FutureTask
es.submit(futureTask);
// 获取计算结果
Integer result = futureTask.get();
// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
// 创建并启动线程
Thread T1 = new Thread(futureTask);
T1.start();
// 获取计算结果
Integer result = futureTask.get();
2. 实现最优的“烧水泡茶”程序
发编程可以总结为三个核心问题:分工、同步和互斥。编写并发程序,首先要做的就是分工,所谓分工指的是如何高效地拆解任务并分配给线程。
用两个线程T1和T2来完成烧水泡茶程序,T1负责洗水壶、烧开水、泡茶这三道工序,T2负责洗茶壶、洗茶杯、拿茶叶三道工序,其中T1在执行泡茶这道工序时需要等待T2完成拿茶叶的工序。
这里需要注意的是ft1这个任务在执行泡茶任务前,需要等待ft2把茶叶拿来,所以ft1内部需要引用ft2,并在执行泡茶之前,调用ft2的get()方法实现等待。
public class MyTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建任务T2的FutureTask
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
// 创建任务T1的FutureTask
FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
// 线程T1执行任务ft1
Thread thread1 = new Thread(ft1);
thread1.start();
// 线程T2执行任务ft2
Thread thread2 = new Thread(ft2);
thread2.start();
// 等待线程T1执行结果
String tea = ft1.get();
System.out.println(tea);
}
}
public class T1Task implements Callable<String> {
FutureTask<String> ft2;
public T1Task(FutureTask<String> ft2) {
super();
this.ft2 = ft2;
}
@Override
public String call() throws Exception {
System.out.println("T1 洗水壶---");
TimeUnit.SECONDS.sleep(1);
System.out.println("T1 烧开水---");
TimeUnit.SECONDS.sleep(15);
// 获得线程T2的茶叶
String teaLeaf = ft2.get();
System.out.println("T1 拿到茶叶:" + teaLeaf);
System.out.println("T1 泡茶:" + teaLeaf);
return "上茶:" + teaLeaf;
}
}
public class T2Task implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("T2 洗茶壶---");
TimeUnit.SECONDS.sleep(1);
System.out.println("T2 洗茶杯---");
TimeUnit.SECONDS.sleep(2);
System.out.println("T2 拿茶叶");
TimeUnit.SECONDS.sleep(1);
return "铁观音";
}
}
最后结果
T1 洗水壶—
T2 洗茶壶—
T1 烧开水—
T2 洗茶杯—
T2 拿茶叶
T1 拿到茶叶:铁观音
T1 泡茶:铁观音
上茶:铁观音
3.总结
利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用Future来解决。在分析这种问题的过程中,建议你用有向图描述一下任务之间的依赖关系,同时将线程的分工也做好,类似于烧水泡茶最优分工方案那幅图。对照图来写代码,好处是更形象,且不易出错。
4.课后思考
不久前听说小明要做一个询价应用,这个应用需要从三个电商询价,然后保存在自己的数据库里。核心示例代码如下所示,由于是串行的,所以性能很慢,你来试着优化一下吧。
// 向电商S1询价,并保存
r1 = getPriceByS1();
save(r1);
// 向电商S2询价,并保存
r2 = getPriceByS2();
// 向电商S3询价,并保存
r3 = getPriceByS3();