23. Future:如何用多线程实现最优的“烧水泡茶”程序?- 并发工具类

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()方法之间的区别在于方法参数不同,

  1. submit(Runnable task) :参数是一个Runnable接口,Runnable接口的run()方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的Future仅可以用来断言任务已经结束了,类似于Thread.join()。
  2. submit(Callable task):参数是一个Callable接口,它只有一个call()方法,并且这个方法是有返回值的,所以这个方法返回的Future对象可以通过调用其get()方法来获取任务的执行结果。
  3. 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();

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值