文章目录
CompletableFuture
Future接口理论知识复习
-
Future接口是jdk5开始支持的,CompletableFuture是jdk8开始支持的
-
Future接口(FutureTask实现类)定义了操作异步任务执行的一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕
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, TimeoutException; }
-
Future接口作用:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其它事情了,忙其它事情或者先执行完,过一会才去获取子线程的执行结果或变更的任务状态(老师上课时间想喝水,他继续讲课不结束上课这个主线程,让学生去小卖部帮老师买水完成这个耗时和费力的任务)
-
一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务
Future接口常用实现FutureTask异步任务
Future接口能干什么
Future是Java5新加的一个接口,它提供一种异步并行计算的功能,如果主线程需要执行一个很耗时的计算任务,我们会就可以通过Future把这个任务放进异步线程中执行,主线程继续处理其他任务或者先行结束,再通过Future获取计算结果
相关接口
Runnable接口
Callable接口
Future接口和FutureTask实现类
目的
异步多线程任务执行且返回有结果,即三个特点:多线程、有返回、异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
Runnable接口和Callable接口区别
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
}
可以看到实现Runnable接口和Callable接口后重写的方法的区别就是是否有返回值和是否抛异常,根据前面的目的,只有实现Callable接口,因为Callable接口有返回值才符合
Thread类构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
但是查看Thread类构造方法可以发现,想要创建一个线程接受的参数只有Runnable接口,而Runnable接口不满足有返回、异步任务的特点
所以需要找到一个接口,同时满足多线程、有返回、异步任务三个特点
最先想到就是找Runnable接口的子接口,要满足多线程和异步任务,可以看到子接口RunnableFuture接口,实现了Runnable接口、Future接口。但是还需要有返回值,可以看到FutureTask类,实现了RunnableFuture接口,但是FutureTask类并没有实现Callable接口,但是有一种设计模式,构造注入!
本源的Future接口相关架构
Future接口的继承关系
构造方法
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
Code
package com.fastech.juc;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(futureTask.get());
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("---come in call()---");
return "hello Callable";
}
}
输出结果:
---come in call()---
hello Callable
Future编码实战和优缺点分析
优点
Future+线程池异步多线程任务配合,能显著提高程序的执行效率
案例
package com.bilibili.juc.completablefuture;
import java.util.concurrent.*;
public class FutureThreadPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
m1();
// 3个任务,目前开启多个异步任务线程来处理,请问耗时多少?
ExecutorService threadPool = Executors.newFixedThreadPool(3);
long startTime = System.currentTimeMillis();
FutureTask<String> futureTask1 = new FutureTask<>(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task1 over";
});
threadPool.submit(futureTask1);
FutureTask<String> futureTask2 = new FutureTask<>(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task2 over";
});
threadPool.submit(futureTask2);
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("3个任务,目前开启多个异步任务线程来处理,耗时\t---costTime:" + (endTime - startTime) + "毫秒"); // ---costTime:869毫秒
System.out.println(Thread.currentThread().getName() + "\t ---end");
threadPool.shutdown();
// // 这种方式每增加一个任务都需要创建一个线程,所以可以使用线程池实现
// FutureTask<String> futureTask1 = new FutureTask<>(() -> {
// try {
// TimeUnit.MILLISECONDS.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// return "task1 over";
// });
// Thread t1 = new Thread(futureTask1, "t1");
// t1.start();
}
private static void m1() {
// 3个任务,目前只有一个线程main来处理,请问耗时多少?
long startTime = System.currentTimeMillis();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("3个任务,目前只有一个线程main来处理,耗时\t---costTime:" + (endTime - startTime) + "毫秒"); // ---costTime:1112毫秒
System.out.println(Thread.currentThread().getName() + "\t ---end");
}
}
输出结果:
3个任务,目前只有一个线程main来处理,耗时 ---costTime:11021毫秒
main ---end
task1 over
task2 over
3个任务,目前开启多个异步任务线程来处理,耗时 ---costTime:8248毫秒
main ---end
小总结
可以看到通过Future+线程池异步多线程任务配合,几乎可以节约三分之一的性能
缺点1
get()方法容易阻塞
案例
package com.fastech.juc.completablefuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class FutureAPIDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "\t ---come in");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + "\t --- 忙其它任务了");
// System.out.println(futureTask.get()); // 一旦调用get()方法,就会非要等到结果才会离开,不管是否计算完成,容器程序阻塞
System.out.println(futureTask.get(3, TimeUnit.SECONDS)); // 不愿意等待很长时间,我希望过时不候,可以自动离开,这里会抛TimeoutException异常,程序可以通过抓取这个异常避免阻塞
}
}
输出结果:
main --- 忙其它任务了
t1 ---come in
Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
at com.fastech.juc.completablefuture.FutureAPIDemo.main(FutureAPIDemo.java:32)
小总结
- get方法容易阻塞,一般放在程序后面。一旦调用get()方法,就会非要等到结果才会离开,不管是否计算完成,容器程序阻塞
- 假如不愿意等待很长时间,希望过时不候,可以自动离开,使用
get(long timeout, TimeUnit unit)
方法,超时未获取到结果会抛TimeoutException异常,程序可以通过抓取这个异常避免阻塞
缺点2
isDone()方法轮询,容易导致cpu空断,耗费更多的性能资源
案例
package com.fastech.juc.completablefuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class FutureAPIDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "\t ---come in");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + "\t --- 忙其它任务了");
while (true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在处理中。。。");
}
}
}
}
输出结果:
main --- 忙其它任务了
t1 ---come in
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
正在处理中。。。
task over
小总结
- 轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果
- 如果想要异步获取结果,通常都会以轮询地方式去获取结果,尽量不要阻塞
结论
Future对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果
想完成一些复杂的任务
对于简单的业务场景使用Future完全OK,但是对于高并发复杂的业务场景,需要不停的轮询或者容易导致阻塞,或多或少都有一些潜在的风险,另外,我们更希望Future这个异步任务获取返回值功能更加强大!具体需求如下
回调通知
应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
通过轮询的方式去判断任务是否完成这样非常占CPU并且代码也不优雅
创建异步任务
尽量把复杂的交给异步任务,主干还是专注自己的业务逻辑,不牵扯在一起,可以使用前面演示的Future+线程池配合实现
多个任务前后依赖可以组合处理(水煮鱼)
想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
将两个或多个异步计算合成一个异步计算,这几个异步计算相互独立,同时后面这个又依赖前一个处理的结果
例如:ps -ef|grep tomcat,这个linux系统的命令就是在ps -ef结果集中找到带有tomcat的结果集
对计算速度选最快
当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果。
例如:两个人比赛玩消消乐,谁先完成就显示谁win
…
CompletableFuture引入
通过上面想完成一些复杂的任务,使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture以声明式的方式优雅的处理这些需求
从i(Future)到i++(CompletableFuture),Future能干的,CompletableFuture都能干!
CompletableFuture对Future的改进
CompletableFuture为什么出现
- get()方法在Future计算完成之前会一直处于阻塞状态下,阻塞的方式和异步编程的设计理念相违背
- isDone()方法容易耗费CPU资源(CPU空转)
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果了
jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。