1、Future接口理论知识复习
Future接口定义了操作==异步任务执行的一些方法==,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等等。
主线程在运行的过程中,有其他的事情需要去处理,但是因为主线程很重要不能中断,因此需要委派一个子线程去帮忙执行,等到主线程忙完了自己的事情之后,才去获取子线程运行的结果。 比如在教室里老师在上课,老师上课是主线程,老师需要去买瓶水喝,但是教学任务(主线程的任务)不能中断,因此委派班长(子线程)去帮忙买水,等到老师讲完课之后,再从班长的手中拿到这瓶水。
即Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。
2、Future接口常用实现类FutureTask异步任务
Future提供了一种异步并行计算的功能。 主线程在运行的过程中,有其他的事情需要去处理,但是因为主线程很重要不能中断,因此需要委派一个子线程去帮忙执行,等到主线程忙完了自己的事情之后,才去获取子线程运行的结果。 因此我们的目标是 ==异步多线程任务执行并返回结果==,即==多线程==/==有返回==/==异步任务==。
2.1 FutureTask的来源过程
根据上述三个特点,==多线程==/==有返回==/==异步任务==,我们一步一步来。 首先对于多线程,我们可以实现Runnable接口或者是Callable接口。
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("MyThread run");
}
}
class MyThread2 implements Callable{
@Override
public Object call() throws Exception {
return null;
}
}
而Runnable和Callable的区别在于,Runnable调用的是run方法,Callable调用的是call方法且会抛异常同时有返回值。 所以,Callable能满足==多线程==和===有返回===两个条件。 但是Thread类中只能传入Runnable接口的实现类
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 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);
}
所以不能通过使用Callable,只能使用Runnable接口。 所以观察Runnable接口,可以发现其子接口有RunnableFuture,前面提到,Future是可以实现异步任务的,所以RunnableFuture可以满足==多线程==和==异步任务==的特点,但是还少了一个==有返回==,观察RunnableFuture的实现类可以发现,有一个FutureTask,而在FutureTask中有一个构造器,可以用于传入Callable接口的实现类对象。
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
因此,我们可以使用FutureTask并传入Callable接口的实现类对象来达到我们想要的目标:==异步多线程任务执行并返回结果==。 最终用代码验证一下我们的想法:
public class FutureTaskOriginal {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask<>(new MyThread2());
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(futureTask.get());
}
}
class MyThread2 implements Callable{
@Override
public Object call() throws Exception {
System.out.println("MyThread2 call Callable");
return "hello Callable";
}
}
运行结果为:
MyThread2 call Callable
hello Callable
2.2 FutureTask的优缺点
2.2.1 优点
==Future+线程池异步多线程任务配合,能显著提高程序的执行效率==
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime " + (endTime - startTime) + "毫秒");
}
最终花费的时间为
----costTime 1136毫秒
而如果我们使用FutureTask线程池的话
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
long startTime = System.currentTimeMillis();
FutureTask<String> futureTask1 = new FutureTask<>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "futureTask1 over";
});
threadPool.submit(futureTask1);
FutureTask<String> futureTask2 = new FutureTask<>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "futureTask2 over";
});
threadPool.submit(futureTask2);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime " + (endTime - startTime) + "毫秒");
}
此时耗费的时间为
----costTime 344毫秒
假设我们还需要去获取返回值
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
long startTime = System.currentTimeMillis();
FutureTask<String> futureTask1 = new FutureTask<>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "futureTask1 over";
});
threadPool.submit(futureTask1);
FutureTask<String> futureTask2 = new FutureTask<>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "futureTask2 over";
});
threadPool.submit(futureTask2);
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime " + (endTime - startTime) + "毫秒");
}
此时耗费的时间为
futureTask1 over
futureTask2 over
----costTime 860毫秒
可以看到,比之前使用单线程时的时间少了1/3。
2.2.2 缺点
① get()方法导致阻塞
FutureTask.get()方法会导致阻塞
public class FutureApi {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "futureTask over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + "----> main线程去处理其他的");
System.out.println(futureTask.get());
}
}
上述代码是在启动完t1线程之后,main线程去处理其他的事情,接着才去获取t1线程执行的结果,这样子main线程能顺利的去处理其他的。 最终运行的结果是:且两者之间没有任何停顿
main----> main线程去处理其他的
futureTask over
而如果将处理其他的和获取t1线程执行的结果的顺序进行调换的话,即
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName() + "----> main线程去处理其他的");
那么,最终执行时则会先停顿时间,等待get()方法调用完成后,main线程才能去处理其他的事情。
# 中间有停顿
futureTask over
main----> main线程去处理其他的
这种情况下会导致,main线程需要去等待get()方法调用完才能去处理其他的事情,这会降低程序的执行效率,也就是get()会导致线程阻塞。
② get()方法导致阻塞的改良
不过,get()也有相应的措施用于减少这种情况出现时的损失。
public V get(long timeout, TimeUnit unit);
前面是超时的数值,后面是超时的单位,如果get()在等待了相应的时间之后,会抛出异常终止继续获取,从而降低损失。
System.out.println(Thread.currentThread().getName() + "----> main线程去处理其他的");
System.out.println(futureTask.get(3,TimeUnit.SECONDS));
此时运行的结果为:
main----> main线程去处理其他的
Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
at com.linhanji.juc.completablefuture.FutureApi.main(FutureApi.java:27)
抛出异常之后采取相应措施,可以降低损失。 但是这种方式会导致一个问题:会抛出异常,导致线程的终止,这是不大利于系统的运行的。
③ isDone()轮询的方式
while (true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
System.out.println("还在处理中,请稍等!!!");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
将get()方法封装在isDone()当中,这样即可以查询结果,又不会抛出异常。
运行的结果为:
main----> main线程去处理其他的
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
还在处理中,请稍等!!!
futureTask over
Process finished with exit code 0
只是,这种方式会导致CPU资源的浪费,在轮询的过程中会消耗CPU资源。
2.3 一些复杂的业务逻辑
- 回调通知
- 创建异步任务
- 多个任务前后依赖组合处理
- 计算速度最优化 这些复杂的业务逻辑,单凭一个Future可能解决不了,Future接口内部提供的方法只有几个。
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
上述的这些方法,根本解决不了我们想要的复杂业务逻辑,因此,我们需要有一个其他的接口来完成这些功能。而这,就引出了CompletableFuture。
3、CompletableFuture对Future接口的改进
3.1 CompletableFuture为什么会出现
get()会导致阻塞,isDone()会造成CPU资源的浪费。
对于真正的异步处理,我们希望通过传入回调函数,在Future结束时自动调用回调函数,就不需要进行等待。
因此设计出CompletableFuture,CompletableFuture是基于观察者模式进行设计的,让任务执行完毕后通知监听的一方。
3.2 CompletableFuture和CompletionStage源码分析
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}
通过源码可以看到,CompletableFuture即实现了Future接口又实现了CompletionStage。即有Future的功能还额外多了CompletionStage的功能。
3.2.1 CompletionStage
通过源码可以看到,CompletionStage定义了许多的接口(这里只列举了一部分,还有其他的),足以见得CompletableFuture的功能比Future的功能还要多。
CompletionStage代表异步计算过程中的某个阶段,当这个阶段结束之后才能接着去执行下一个阶段(触发下一个阶段)。一个阶段的执行可以是被单个阶段触发,也可以被多个阶段共同触发的。类似于Linux当中的管道符
ps -ef | grep tomcat
只有当ps -ef执行了之后,grep tomcat才能在前面执行的基础上接着执行。
3.2.2 CompletableFuture
CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。 CompletableFuture根据其实现的接口,可以代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
3.3 四个静态方法创建异步任务
3.3.1 空参构造器的方式
直接new对象的方式创建异步任务
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
只是这种不方法不推荐使用。官方文档中为空参构造器作了解释,创建一个不完全的CompletableFuture,只是为了语法需要而定义的构造器,但本身是不能合格的。
/**
* Creates a new incomplete CompletableFuture.
*/
public CompletableFuture() {}
3.3.2 四个静态方法创建异步任务
① runAsync无返回值的方式
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor) {
return asyncRunStage(screenExecutor(executor), runnable);
}
② supplyAsync有返回值的方式
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
上述的两种方法中,各有一个构造器需要传入一个Executor executor,这个参数是用于指定线程池来执行异步代码的,如果没有传入这个参数,则默认是ForkJoinPool,如果有传入,则按照传入的线程池执行异步代码。
3.3.3 实操
① runAsync无返回值
Ⅰ、无Executors的创建方式
代码:
private static void runAsyncWithoutExecutorTest() throws InterruptedException, ExecutionException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
System.out.println(Thread.currentThread().getName());
// 暂停1秒钟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(completableFuture.get());
}
此时执行的结果:
ForkJoinPool.commonPool-worker-9
null
可以看到,此时的线程池是ForkJoinPool.commonPool-worker-9,默认的线程池。
异步任务的执行结果输出为null,也确实是没有返回值
Ⅱ、有Executors的创建方式
代码
private static void runAsyncWithExecutorsTest() throws InterruptedException, ExecutionException {
// 自定义的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, threadPool);
System.out.println(completableFuture.get());
// 关闭线程池
threadPool.shutdown();
}
运行的结果:
pool-1-thread-1
null
可以看到,此时的线程池不再是ForkJoinPool,而是自己定义的pool,同样的,返回值也是null。
② supplyAsync有返回值
Ⅰ、无Executors的创建方式
代码:
private static void supplyAysncWithoutExecutorsTest() throws InterruptedException, ExecutionException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "hello supplyAsync without executors!!";
});
System.out.println(completableFuture.get());
}
执行结果:
ForkJoinPool.commonPool-worker-9
hello supplyAsync without executors!!
Ⅱ、有Executors的创建方式
代码:
private static void supplyAsyncWithExecutorsTest() throws InterruptedException, ExecutionException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "hello supplyAsync with executors!!";
},threadPool);
System.out.println(completableFuture.get());
threadPool.shutdown();
}
执行结果是:
pool-1-thread-1
hello supplyAsync with executors!!
3.4 通用异步编程
3.4.1 使用CompletableFuture完成Future的编程
private static void CompletableFutureInsteadFutureUsing() throws InterruptedException, ExecutionException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "--------- come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------ 1秒钟之后出结果:" + result);
return result;
});
System.out.println(Thread.currentThread().getName() + " 线程先去忙其他任务");
System.out.println(completableFuture.get());
}
此时运行的结果:
main 线程先去忙其他任务
ForkJoinPool.commonPool-worker-9--------- come in
------ 1秒钟之后出结果:3
3
可以看到,其实跟Future的使用方法一样,且输出结果一样。
3.4.2 不同于Future的CompletableFuture编程
private static void unfamiliarFutureUsedCompletableFuture() {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "--------- come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------ 1秒钟之后出结果:" + result);
return result;
}).whenComplete((v,e) -> {
if (e == null) {
System.out.println("------计算完成,更新系统:" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:" + e.getCause() + "\t" + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName() + " 线程先去忙其他任务");
}
区别点在于,调用完了supplyAysnc()方法之后还会接着调用whenComplete()和exceptionally()方法。
① whenComplete()
当前面的异步任务supplyAsync()正常执行结束之后执行的方法,在参数列表中有(v,e),v是前面异步任务执行的返回值即result,e是前面异步任务执行过程中可能抛出的异常。
② exceptionally()
当前面的异步任务supplyAsync()方法执行的过程中出现了异常时执行的方法,这里的e仍然是抛出的异常。
③ 出现的问题
此时执行的结果是:
ForkJoinPool.commonPool-worker-9--------- come in
main 线程先去忙其他任务
可以发现,此时应该是正常执行结束的,但是却没有执行whenComplete()方法,这是因为,当主线程结束时,CompletableFuture的默认线程池会立即关闭,而默认线程池被关闭后,异步任务就不能正常执行了,所以就不会接着走。
因此,我们有两种解决方法,一种是不使用默认线程池即自定义线程池,另一种则是预判异步任务需要执行的时间,然后让主线程在这段时间内不要结束。
Ⅰ、让主线程在这段时间内不要结束
private static void unfamiliarFutureUsedCompletableFutureBySleepMain() {
...
System.out.println(Thread.currentThread().getName() + " 线程先去忙其他任务");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
此时执行的结果为:
ForkJoinPool.commonPool-worker-9--------- come in
main 线程先去忙其他任务
------ 1秒钟之后出结果:9
------ 计算完成,更新系统:9
Ⅱ、自定义线程池
private static void unfamiliarFutureUsedCompletableFutureByDefinedThreadPool() {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
...
},threadPool).whenComplete((v,e) -> {
if (e == null) {
System.out.println("------ 计算完成,更新系统:" + v);
}
}).exceptionally(e -> {
...
});
System.out.println(Thread.currentThread().getName() + " 线程先去忙其他任务");
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
此时运行的结果为:
pool-1-thread-1--------- come in
main 线程先去忙其他任务
------ 1秒钟之后出结果:2
------ 计算完成,更新系统:2
接着测试一下exceptionally()方法的调用,手动在supplyAysnc()中设置异常现象。
其余代码省略
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 用于测试exceptionally()方法的回调
int i = 10/0;
System.out.println("------ 1秒钟之后出结果:" + result);
return result;
此时执行:
pool-1-thread-1--------- come in
main 线程先去忙其他任务
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
...
Caused by: java.lang.ArithmeticException: / by zero
at ... more
异常情况:java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
可以看到此时执行了exceptionally()的方法,且出异常后剩下的代码也没有执行。
3.5 CompletableFuture的优点
- 异步任务结束时,自动回调某个对象的方法(如前面的
supplyAysnc()结束后自动调用whenCompletion()或者是exceptionally()) - 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错后自动回调某个对象的方法(如前面的
supplyAysnc()出现异常后自动调用exceptionally())
4、案例精讲--从电商网站的比价需求说开去
4.1 函数式编程
Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程
4.1.1 Chain链式调用
先定义一个Student类。
@AllArgsConstructor
@NoArgsConstructor
@Data
// 开启链式调用
@Accessors(chain = true)
class Student {
private Integer id;
private String studentName;
private String major;
}
链式调用是lombok里的一个用法,@Accessors(chain = true)进行开启,默认是关闭的。
而链式调用是指可以在调用某个对象的某个方法之后接着去调用其他的方法。
正常一般调用同一个对象的多个方法是分开写的,如下:
student.setId(1);
student.setMajor("cs");
student.setStudentName("zhangsan");
而链式调用则可以写在一行,如下:
student.setId(2).setMajor("math").setStudentName("lisi");
最终对比一下两者的结果:
Student(id=1, studentName=zhangsan, major=cs)
Student(id=2, studentName=lisi, major=math)
可以看到,两者的输出结果并没有区别,也就是链式调用是可行的。
4.2 join和get的对比
get()和join()方法都是用于获取异步任务执行的结果的。
但是,对于get()用法而言,是需要抛出异常信息的,而join()则不需要抛出异常。
测试get()
private static void testGet() throws InterruptedException, ExecutionException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "hello";
});
System.out.println(completableFuture.get());
}
测试join()
private static void testJoin() {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "hello";
});
completableFuture.join();
}
4.3 案例演示
4.3.1 需求
1、需求:
1.1 同一款产品,同时搜索出同款产品在各大电商平台的售价;
1.2 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
2、输出:出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List<String>
《mysql》 in jd price is 88.05
《mysql》 in dangdang price is 86.11
《mysql》 in taobao price is 90.43
根据需求,简单搭建一下环境
public class CompletableFutureMall {
/**
* 收集各大电商平台
*/
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("dangdang"),
new NetMall("taobao")
);
}
/**
* 电商平台类
*/
class NetMall {
@Getter
private String netMallName;
public NetMall(String netMallName) {
this.netMallName = netMallName;
}
// 计算价格
public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
}
4.3.2 解决方案
针对该需求,可以有两种方法,要查出一个产品在各大电商平台的售价,我们可以一个电商平台一个电商平台去查找,最终将查找结果收集。也可以用异步任务,用相同数量的线程同时查找各大电商平台的售价,最终将查找结果收集。
① 一个一个去查找
/**
* 一步一步查询结果获取的过程
* List<NetMall> ----> Stream<NetMall> ----> Stream<String> ----> List<String>
* @param list 所有的电商平台
* @param productName 需要查询的产品的名字
* @return 返回所有电商平台查询到相关产品的价格的list
*/
public static List<String> getPriceStepByStep(List<NetMall> list, String productName){
return list
// 将list转换成一个Stream流
.stream()
// 把每一个netMall都映射成一个String
.map(netMall ->
String.format(productName + " in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))
// 将所有映射的结果作为一个List
.collect(Collectors.toList());
}
② 同时查找
/**
* 多线程异步查找
* List<NetMall> ----> List<CompletableFuture<String>> ----> List<String>
* @param list 所有的电商平台
* @param productName 需要查询的产品的名字
* @return 返回所有电商平台查询到相关产品的价格的list
*/
public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
return list
.stream()
// map方法是把list集合里的每一个元素作为netMall(这里的名字可以随便定义)
// 将其映射为CompletableFuture
.map(netMall -> CompletableFuture
.supplyAsync(() -> String.format(productName + " in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName))))
// 将所有结果转换成一个List<CompletableFuture<String>>
.collect(Collectors.toList())
// 再将List<CompletableFuture<String>>作为一个Stream流
.stream()
// 再次进行映射,获取CompletableFuture<String>里每个异步任务查询到的结果
.map(s -> s.join())
// 将所有异步任务查询到的结果转换成一个List<String>
.collect(Collectors.toList());
}
测试上述两个方法
public static void main(String[] args) {
long startTime1 = System.currentTimeMillis();
List<String> mysql = getPriceStepByStep(list, "mysql");
for (String s : mysql) {
System.out.println(s);
}
long endTime1 = System.currentTimeMillis();
System.out.println("-------- costTime: " + (endTime1 - startTime1) + " 毫秒");
System.out.println("--------------------------------------------------------");
long startTime2 = System.currentTimeMillis();
List<String> mysql2 = getPriceByCompletableFuture(list, "mysql");
for (String s : mysql2) {
System.out.println(s);
}
long endTime2 = System.currentTimeMillis();
System.out.println("-------- costTime: " + (endTime2 - startTime2) + " 毫秒");
}
此时运行的结果为:
mysql in jd price is 110.22
mysql in dangdang price is 109.80
mysql in taobao price is 110.88
-------- costTime: 3090 毫秒
---------------------------------------------------------
mysql in jd price is 109.33
mysql in dangdang price is 110.76
mysql in taobao price is 109.11
-------- costTime: 1018 毫秒
可以看到,异步任务的执行效率比一步一步的执行效率高了很多。 如果此时添加电商平台
/**
* 收集各大电商平台
*/
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("dangdang"),
new NetMall("taobao"),
new NetMall("tmall"),
new NetMall("pdd")
);
此时的执行结果为:
mysql in jd price is 110.92
mysql in dangdang price is 110.03
mysql in taobao price is 110.55
mysql in tmall price is 110.23
mysql in pdd price is 110.73
-------- costTime: 5120 毫秒
---------------------------------------------------------
mysql in jd price is 109.17
mysql in dangdang price is 109.96
mysql in taobao price is 110.05
mysql in tmall price is 110.01
mysql in pdd price is 109.36
-------- costTime: 1012 毫秒
可以看到,对于异步任务而言其执行时间并没有什么变化,而一步一步的执行效率会随着电商平台的增加而变慢。
5、CompletableFuture常用方法
5.1 获取结果和触发计算
5.1.1 获取结果
① public T get()
可以看到,这里get()需要抛出异常,而且会导致阻塞
/**
* 测试get()方法
* @throws InterruptedException
* @throws ExecutionException
*/
private static void testGet() throws InterruptedException, ExecutionException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
System.out.println(completableFuture.get());
}
② public T get(long timeout, TimeUnit unit)
/**
* 测试get()方法的超时机制,一旦在规定时间里没有获得结果则直接抛出异常
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
private static void testGetWithTimeOut() throws InterruptedException, ExecutionException, TimeoutException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
completableFuture.get(1,TimeUnit.SECONDS);
}
③ public T join()
不会阻塞其他线程,会等到结果出来再获取,也不需要抛出异常
/**
* 测试join()方法
*/
private static void testJoin() {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
System.out.println(completableFuture.join());
}
④ public T getNow(T valueOfIfAbsent)
立刻获取结果,如果此时还没有计算完,则返回设置的默认值valueOfIfAbsent。
/**
* 测试getNow()方法
*/
private static void testGetNow() {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
System.out.println(completableFuture.getNow("No Complete Return Default Value"));
}
此时还未计算完成,因此运行后的结果为:
No Complete Return Default Value
如果此时已经计算完成了,那么运行后的结果为:
CompletableFutureApiTest
5.1.2 主动触发计算
① public boolean complete(T value)
如果调用complete方法时异步任务执行完了,则不进行打断,返回值为false。
如果调用complete方法时异步任务还未执行完,那么进行打断,返回值为true,并且将该异步任务的返回值设置为value。
Ⅰ、异步任务执行完了的情况
/**
* 测试Complete()方法
*/
private static void testComplete() {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(completableFuture.complete("completeValue") + "\t" + completableFuture.join());
}
此时在调用Complete()方法时,已经计算完了,那么输出的结果为:
false CompletableFutureApiTest
false表示没有打断,即异步任务正常执行完,后面的CompletableFutureApiTest是返回值。
Ⅱ、异步任务未执行完的情况
private static void testComplete() {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "CompletableFutureApiTest";
});
System.out.println(completableFuture.complete("completeValue") + "\t" + completableFuture.join());
}
此时运行的结果为:
true completeValue
此时complete()返回的结果为true,说明已经打断了异步任务的执行,异步任务执行的结果为completeValue。
5.2 对计算结果进行处理
5.2.1 thenApply()
thenApply()是将上一个步骤处理的结果作为参数传入,如果遇到了异常信息,则直接抛出,后续遇到相同的thenApply()也会因为异常而无法执行。
private static void testThenApplyWithOrWithoutException() {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("111");
return 1;
},threadPool).thenApply(f -> {
// 异常出现
// int i = 10/0;
System.out.println("222");
return f + 2;
}).thenApply(f -> {
System.out.println("333");
return f + 3;
}).whenComplete((v,e) -> {
if (e == null) {
System.out.println("--------- 计算结果:" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getMessage());
return null; });
System.out.println("--------- 主线程忙其他的去了");
threadPool.shutdown();
}
正常运行的结果:
111
222
333
--------- 计算结果:6
--------- 主线程忙其他的去了
可以看到,整个调用按照顺序执行了
如果此时在某个过程中出现了异常呢?
假设在第二步即输出222这里出现了异常
111
java.lang.ArithmeticException: / by zero
--------- 主线程忙其他的去了
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
...
可以看到,只打印了111,222和333都没有打印出来,也就是当异常发生时,后面的thenApply()方法就不会再接着调用了。
5.2.2 handler()
handler()和thenApply()一样都能将上一步的执行结果作为参数传入,但是handler()比thenApply()多了一个处理异常的能力。当handler()中发生异常时,会中断当前方法的调用将异常抛出,同时,在后面的步骤中如果有handler()存在,该handler()也会继续执行。
private static void testHandlerWithOrWithoutException() {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("thread 111");
return 1;
},threadPool).handle((f,e) -> {
// 异常出现
// int i = 10/0;
System.out.println("handler 222");
return f + 2;
}).thenApply(f -> {
System.out.println("thenApply 333");
return f + 4;
}).handle((f,e) -> {
System.out.println("handler 444");
return f + 3;
}).thenApply(f -> {
System.out.println("thenApply 555");
return f + 5;
}).handle((f,e) -> {
System.out.println("handler 666");
return f + 6;
}).whenComplete((v,e) -> {
if (e == null) {
System.out.println("--------- 计算结果:" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getMessage());
return null;
});
System.out.println("--------- 主线程忙其他的去了");
threadPool.shutdown();
}
在没有异常的情况下,正常执行的结果如下:
thread 111
handler 222
thenApply 333
handler 444
thenApply 555
handler 666
--------- 计算结果:21
--------- 主线程忙其他的去了
Process finished with exit code 0
所有调用按顺序执行。
如果此时在handler()中发生了异常呢,情况又是如何?
假设现在在handler 222中出现了异常
thread 111
handler 444
handler 666
java.lang.NullPointerException
--------- 主线程忙其他的去了
java.util.concurrent.CompletionException: java.lang.NullPointerException
可以看到,当handler 222发生异常时,thenApply()一个都不会执行,而后续的handler()方法都会执行。
thenApply()有点像try{thenApply()}catch(),当异常发生时,后面的代码不会再接着执行。
而handler()方法则有点像try{ }catch(){ }finally{ }
如果执行的过程中没有发生异常,那么整个的执行链如下:
try {
handler(); // handler 222
thenApply(); // thenApply 333
handler(); // handler 444
thenApply(); // thenApply 555
handler(); // handler 666
} catch() {
} finally {
// 没有任何操作
}
如果在执行的过程中发生了异常,那么,从发生异常的handler()往后开始都作为finally()的代码块,假设还是一样,handler 222,发生了异常,那么此时的执行链如下:
try {
handler(); // handler 222 异常发生
// thenApply(); // thenApply 333 这两个都不再执行
// thenApply(); // thenApply 555 不再执行
} catch() {
} finally {
// 没有任何操作
handler(); // handler 444
handler(); // handler 666
}
小结:当异常发生时,后续的thenApply()不会接着执行,而handler()可以接着执行。
5.3 对计算结果进行消费
5.3.1 thenAccept()
thenAccept()对前面的计算结果进行消费使用,且本身是没有返回值的。
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {}
Comsumer: void accept(T t);
private static void testThenAccept() {
CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 3;
}).thenAccept(r -> {
System.out.println(r);
});
}
此时输出的结果为:
6
为前面所执行的结果。
5.3.2 不同消费的对比
对计算结果进行消费的一共有三种
- thenRun(Runnable runnable):任务A执行完执行B,B不依赖于任务A
- thenApply(Comsumer action):任务A执行完执行B,B依赖于任务A的结果,但有返回值
- thenAccept(Function fn):任务A执行完执行B,B依赖于任务A的结果,但无返回值 对比测试:
// thenRun
System.out.println(CompletableFuture.supplyAsync(() -> { return "resultA"; }).thenRun(() -> {}).join() );
System.out.println("================================================");
// thenApply
System.out.println(CompletableFuture.supplyAsync(() -> { return "resultA"; }).thenApply(f -> {return f + " resultB";}).join() );
System.out.println("================================================");
// thenAccept
System.out.println(CompletableFuture.supplyAsync(() -> { return "resultA"; }).thenAccept(r -> {System.out.println(r);}).join() );
通过join()方法来验证是否有返回值。
程序运行的结果为:
# thenRun
null
================================================
# thenApply
resultA resultB
================================================
# thenAccept
resultA
null
可以看到,除了thenApply会将结果进行返回之外,其他的返回结果都为null,也证明了只有thenApply有返回值,其余的没有返回值。
5.3.3 CompletableFuture与线程池说明
观察可以发现,所有xxxXxx()都有对应的xxxXxxAsync(),比如theRun()有对应的theRunAsync(),他们的区别是什么?
以下是本次测试的代码,总的代码是这些,具体有不同的部分到时候会在下面注解
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1号任务\t" + Thread.currentThread().getName());
return "abcd";
}).thenRun(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("2号任务\t" + Thread.currentThread().getName());
}).thenRun(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("3号任务\t" + Thread.currentThread().getName());
}).thenRun(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("4号任务\t" + Thread.currentThread().getName());
});
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
threadPool.shutdown();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
① 使用默认线程池的情况
Ⅰ、thenRun()
此时运行的结果为:
1号任务 ForkJoinPool.commonPool-worker-9
2号任务 ForkJoinPool.commonPool-worker-9
3号任务 ForkJoinPool.commonPool-worker-9
4号任务 ForkJoinPool.commonPool-worker-9
可以看到,是使用的默认的线程池,因为我们没有传入自定义的线程池。
Ⅱ、thenRunAysnc()
如果将上述代码中的thenRun()所有修改为thenRunAysnc()之后
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
...
}).thenRunAysnc(() -> {
...
}).thenRunAysnc(() -> {
...
}).thenRunAysnc(() -> {
...
});
运行的结果如下:
1号任务 ForkJoinPool.commonPool-worker-9
2号任务 ForkJoinPool.commonPool-worker-9
3号任务 ForkJoinPool.commonPool-worker-9
4号任务 ForkJoinPool.commonPool-worker-9
可以看到,运行的结果跟前面的都是一样的。在这种情况下这两者是没有区别的。
② 使用自定义线程池的情况
Ⅰ、thenRun()
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
...
},threadPool).thenRun(() -> {
...
}).thenRun(() -> {
...
}).thenRun(() -> {
...
});
此时运行的结果为
1号任务 pool-1-thread-1
2号任务 pool-1-thread-1
3号任务 pool-1-thread-1
4号任务 pool-1-thread-1
也确实是使用了自定义的线程池
Ⅱ、thenRunAysnc()
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
...
},threadPool).thenRunAsync(() -> {
...
}).thenRunAsync(() -> {
...
}).thenRunAsync(() -> {
...
});
此时运行的结果:
1号任务 pool-1-thread-1
2号任务 ForkJoinPool.commonPool-worker-9
3号任务 ForkJoinPool.commonPool-worker-9
4号任务 ForkJoinPool.commonPool-worker-9
可以看到,除了第一个使用的是自定义的线程外,剩下的都是使用的默认的线程。
而这,就是xxxXxxAsync()与xxxXxx()的区别。
还有下面一种情况
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
// ...
},threadPool).thenRun(() -> {
...
}).thenRun(() -> {
...
}).thenRun(() -> {
...
});
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
threadPool.shutdown();
}
此时的运行结果如何呢?
1号任务 pool-1-thread-1
2号任务 main
3号任务 main
4号任务 main
可以看到,main线程也过来执行了。这是为什么呢? 这是因为,有时候某个异步任务很快就执行完了,而系统有优化切换原则,所以会直接使用main线程进行处理,就不需要再从异步任务的线程池当中取出其中一个进行处理了。
③ 小结
- 没有传入线程池,都默认使用ForkJoinPool
- 传入了自定义线程池
- 对于thenRun而言,后一个使用的线程池与前一个使用的一样
- 对于thenRunAysnc而言,除了第一个使用自定义的,从thenRunAsync开始使用的是ForkJoinPool
- 有时候一个异步任务很快就能处理完,根据系统优化切换原则,会直接使用main线程处理。 而对于其他类似的有相同组合的方法,其情况与上述所列举的一样。
5.4 对计算速度进行选用
applyToEither():用于两个异步任务的执行效率进行比较,效率高执行。
代码实现:
private static void testApplyToEither() {
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "playerA";
});
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "playerB";
});
CompletableFuture<Object> result = playA.applyToEither(playB,f -> {
return f + " is winner";
});
System.out.println(Thread.currentThread().getName() + "\t ----: " + result.join());
}
从代码上可以看出,此时是playA的执行效率高一些,运行结果如下:
main ----: playerA is winner
而修改playA和playB的执行时间
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
...
TimeUnit.MILLISECONDS.sleep(30);
...
});
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
...
TimeUnit.MILLISECONDS.sleep(20);
...
});
...
此时的运行结果为:
main ----: playerB is winner
5.5 对计算结果进行合并
thenCombine()可以对两个异步任务的计算结果进行合并,具体的合并方式自行设定。
5.5.1 分开版
测试thenCombine()用法
private static void testThenCombine() {
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println("第一个任务执行完了,其结果为:10");
return 10;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println("第二个任务执行完了,其结果为:20");
return 20;
});
CompletableFuture<Integer> result = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.print("现在合并两个任务执行的结果,其结果为:");
return x + y;
});
System.out.println(result.join());
}
此时执行的结果为:
第一个任务执行完了,其结果为:10
第二个任务执行完了,其结果为:20
现在合并前面任务执行的结果,其结果为:30
5.5.2 合并版
private static void testThenCombineCollect() {
CompletableFuture<Integer> thenCombine = CompletableFuture.supplyAsync(() -> {
System.out.println("第一个任务执行完了,其结果为:10");
return 10;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println("第二个任务执行完了,其结果为:20");
return 20;
}), (x, y) -> {
return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println("第三个任务执行完了,其结果为:30");
return 30;
}),(x,y) -> {
System.out.print("现在合并前面任务执行的结果,其结果为:");
return x + y;
});
System.out.println(thenCombine.join());
}
此时执行的结果为:
第一个任务执行完了,其结果为:10
第二个任务执行完了,其结果为:20
第三个任务执行完了,其结果为:30
现在合并前面任务执行的结果,其结果为:60
thenCombine()会累加前面异步任务执行的结果,并对结果进行合并。