文章目录
之前在线程池的使用时,已经了解到当提交的任务需要获取返回值的时候,就需要用到submit这个方法进行任务的提交,而不是execute方法;
1. 线程池submit方法解析(Future接口)
Java通过ThreadPoolExecutor提供的3个submit()方法和一个FutureTask工具类来支持获得任务执行结果的需求,这三个方法如下:
//1. 提交Callable任务,返回的Future,可以通过它的get方法获取Callable的返回值
<T> Future<T> submit(Callable<T> task);
//2. 提交Runnable任务,由于Runnable没有返回值,所以这个方法返回的Future仅可以用来断言任务已经结束 //了
Future<?> submit(Runnable task);
//3. 提交Runnable任务及结果引用
<T> Future<T> submit(Runnable task, T result);
我们会发现上面几个方法的返回值都是Future接口,那么来看看Future这个接口里面有些啥:
(Future也是juc包下的一个接口)
-
boolean cancel(boolean mayInterruptIfRunning); //取消任务
-
boolean isCancelled(); //判断任务是否取消
-
boolean isDone(); //判断任务是否结束
-
V get() throws InterruptedException, ExecutionException; //获得任务执行结果
-
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; //获得任务执行结果, //支持超时
这里我们来简要分析一下那三个submit方法:
- 第一个方法提交Callable任务,返回的Future,可以通过它的get方法获取Callable的返回值;
- 第二个方法提交Runnable任务,由于Runnable没有返回值,所以这个方法返回的Future仅可以用来断言任务已经结束了;
- ⭐第三个就比较复杂点了,可以看到有两个参数,那么这个方法含义是什么?这里先把含义展示出来:
假设这个方法返回的Future对象是f,那么调用f.get()的返回值就是传给submit()方法的参数result;
对于上面的方法三,我们知道了其含义,但它的使用场景一般是什么,下面先来看一个典型代码:
class Result {
int data;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}
class Task implements Runnable {
Result result;
public Task(Result result) {
this.result = result;
}
@Override
public void run() {
int a = result.getData();
result.setData(20);
}
}
public class Test1 {
public static void main(String[] args) throws Exception{
ExecutorService executor = Executors.newFixedThreadPool(1);
Result result = new Result();
result.setData(10);
System.out.println(result.getData());
Future<Result> future = executor.submit(new Task(result), result);
//get方法的返回值就是我们传入的result
result = future.get();
System.out.println(result.getData());
}
}
输出:
10
20
🌞你或许会感到疑惑,上面这个代码的意图是干啥,这里我们做一下 解释: 🌞
- 上面的Task这个任务类实现了一个构造方法,传入了一个Result对象,这一点很关键,保证了能在任务中对result对象进行相关操作,而第二个参数正是这个result对象,当我们调用get方法时,就能获取到这个result对象,这样做的意义就是:result相当于主线程和子线程之间的桥梁,通过它主子线程可以进行共享数据;
2. FutureTask类
1. API概览
首先得知道,FutureTask是一个实体类,而不像Future是一个接口,这个类也是juc包下的,下面首先来看看它的继承关系:
public class FutureTask<V> implements RunnableFuture<V>
继续看看RunnableFuture这个接口:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
看到这,其实就是FutureTask这个类同时实现了Runnable接口和Future接口,那么代表:
- 实现了Runnable接口,所以可以将FutureTask对象作为任务提交给线程池去执行,也可以直接被Thread执行;
- 实现了Future接口,所以也能用来获取任务的执行结果;(get()方法)
下面来看看它的构造方法:(两个)
//传入一个Callable类,即支持返回值;
public FutureTask(Callable<V> callable)
//这个构造方法是不是跟上面的那个submit的一个很像啊
public FutureTask(Runnable runnable, V result)
可以看到,这个类没有无参构造,必须调用上面两种的一种;
2. 简单使用栗子
方式一:将FutureTask对象提交给线程池去执行;
public class Test1 {
public static void main(String[] args) throws Exception{
//构造传入一个Callable,这里直接传入Lambda表达式,注意只有一行时return是可以省略的;
FutureTask<Integer> futureTask = new FutureTask<>(()->1+2);
ExecutorService executorService = Executors.newFixedThreadPool(1);
//用submit方式提交任务,submit方法有一个重载是支持传入一个Runnable;
executorService.submit(futureTask);
int res = futureTask.get();
System.out.println(res);
}
}
输出:3
方式二:直接被Thread执行:
public class Test1 {
public static void main(String[] args) throws Exception{
//构造传入一个Callable,这里直接传入Lambda表达式,注意只有一行时return是可以省略的;
FutureTask<Integer> futureTask = new FutureTask<>(()->1+2);
Thread thread = new Thread(futureTask);
thread.start();
int res = futureTask.get();
System.out.println(res);
}
}
可以看到,使用FutureTask类可以很轻松的拿到子线程的执行结果;
3. 实现最优版的烧水泡茶
著名数学家华罗庚的一篇文章《统筹方法》中,提到了最优的工序应该是下面这个样子:
对于上面这个图,我们用程序来模拟实现一下这个过程;
对于这个烧水泡茶程序,最重要的部分就是分工问题,这里我们可以用两个线程来完成上面的烧水泡茶,线程T1负责洗水壶、烧开水、泡茶,而线程T2实现洗茶壶、洗茶杯、拿茶叶;这里我们应当注意到,T1必须等到T2拿到茶叶后,才能执行泡茶这一步骤,这一块涉及的就是线程之间的同步(通信)问题啦;
对于这个等待操作,可以有很多种方式去实现,例如:
- Thread.join()
- CountDownLatch(闭锁)
- 阻塞队列
本次就来使用刚刚学的FutureTask类来实现这个功能:
class T1 implements Callable<String> {
FutureTask futureTask ;
public T1(FutureTask futureTask) {
this.futureTask = futureTask;
}
public void kettle() throws Exception{
System.out.println("T1:洗水壶......(1s)");
Thread.sleep(1000);
}
public void boilWater() throws Exception{
System.out.println("T1:烧开水......(5s)");
Thread.sleep(3000);
}
public void makeTea() throws Exception{
//泡茶前必须先拿到茶叶
System.out.println("T1:等待T2的茶叶......");
//重点在这⭐⭐,这里将会阻塞,直到取到结果;
String tea = (String) futureTask.get();
System.out.println("T1:拿到了:"+tea);
System.out.println("T1:开始泡茶啦!!");
System.out.println("T1:上茶!!!");
}
@Override
public String call() throws Exception {
kettle();
boilWater();
makeTea();
return null;
}
}
class T2 implements Callable<String> {
public void washTeaPot() throws Exception{
System.out.println("T2:洗茶壶......(1s)");
Thread.sleep(1000);
}
public void washTeabottle() throws Exception{
System.out.println("T2:洗茶杯......(1s)");
Thread.sleep(1000);
}
public String takeTea() throws Exception{
System.out.println("T2:开始取茶叶啦,准备给T1......(4s)");
Thread.sleep(4000);
System.out.println("T2:取到啦,给T1了");
return "上好的龙井!!";
}
@Override
public String call() throws Exception {
washTeaPot();
washTeabottle();
return takeTea();
}
}
public class Test1 {
public static void main(String[] args) throws Exception{
//T2实现洗茶壶、洗茶杯、拿茶叶
FutureTask<String> futureTaskT2 = new FutureTask<>(new T2());
//T1负责洗水壶、烧开水、泡茶
FutureTask futureTaskT1 = new FutureTask(new T1(futureTaskT2));
new Thread(futureTaskT1).start();
new Thread(futureTaskT2).start();
}
}
输出:
T1:洗水壶…(1s)
T2:洗茶壶…(1s)
T1:烧开水…(5s)
T2:洗茶杯…(1s)
T2:开始取茶叶啦,准备给T1…(4s)
T1:等待T2的茶叶…
T2:取到啦,给T1了
T1:拿到了:上好的龙井!!
T1:开始泡茶啦!!
T1:上茶!!!Process finished with exit code 0
这里可以简单的总结一下:
利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的结果,这种问题就可以利用Future来解决;
3. CompletableFuture类
1. 异步化
利用多线程来优化性能,其实不过就是将串行操作变成并行操作;如果仔细观察,你还会发现在串行转换成并行的过程中,一定会涉及到异步化;
例如下面两个方法,我们如何将它并行化达到性能的提升:
//这是两个耗时操作
fun1();
fun2();
很轻松想到,开两个线程分别去执行它们就行了;
当两个线程分别去执行这两个方法时,这两个方法的执行就不再相互依赖,此时这两个方法就已经被异步化 了;
2. API概览
先来看看这个类的定义,可以看到继承了Future类;
public class CompletableFuture<T> implements Future<T>, CompletionStage<T>
3. 如何创建CompletableFuture对象
创建CompletableFuture对象主要依靠下面的这四个静态方法