前言
JDK8的新特性真的是非常多,今天看到一个特别好用的并发工具类CompletableFuture
,学会了这个可以说多线程代码是非常好写了,于是学习了一下,这边跟随B站上一个宝藏博主的视频学习,点击宝藏博主链接可以直达他的主页。
下面主要以CompletableFuture
的几个API结合各种场景展开。
工具类
我们在看下面代码前先看一下工具类的作用
1 | public class CommonUtil { |
SupplyAsync
场景:
小明进门点菜,点完菜之后厨师开始做菜,同时小明打王者进行等待,等厨师做完菜之后小明开始吃饭。
1 | public class OpenAsync { |
1 | 1624810471194-1-main-小明进门 |
Compose
场景:
小明进门点菜,这时厨师开始做菜,等做完菜之后需要服务员打饭,在此期间小明可以玩王者等待,等到服务员打饭成功后,小明开始吃饭。
1 | public class Compose { |
1 | 1624810596064-1-main-小明进门 |
thenCompose
方法的结果是上一个任务的跑完之后把结果交给下一个异步任务也就是说上一个异步任务跑完之后才会开启下一个异步任务。
Combine
场景:
小明进门点菜,这时饭店饭还没蒸好,所以厨师开始做菜的同时,服务员需要开始蒸饭,小明打王者等待等到厨师和服务员,蒸完饭做完菜,并且服务员打完饭,小明开始吃饭。
我们使用之前的学过的知识也可以做到
1 | public static void other() { |
1 | 1624810758571-1-main-小明进门 |
但是相对来说代码不够优雅,这边有更好的API让代码变得更加优雅。
1 | public static void main(String[] args) { |
1 | 1624810821940-1-main-小明进门 |
thenCombine
的作用就是等两个任务都执行完之后得到两个结果,再把两个结果加工成一个结果。
Apply
场景:
小明买单,把钱付给服务员并要求开发票,服务员收到款之后就去开发票,同时小明也没有闲着,接了个电话准备回去开黑,刚接完电话服务员就过来,于是小明就走出餐厅,准备回家。
1 | public class Apply { |
1 | 1624810955359-1-main-小明吃好了 |
ApplyToEither
场景:
小明走出餐厅,到公交站,现在两个公交车都能回家一个700路一个800路。现在小明是哪路先来就做哪路,用代码如何描述。
1 | public class ApplyToEither { |
1 | 1624811044152-1-main-小明走出餐厅, 来到公交站 |
applyToEither
的作用就是上个任务和这个任务一起运行,那个先运行结束,就把任务结果交给Function。
Exceptionally
场景:
小明做800路公交回家,又拿起了电话和朋友聊起了游戏,这时候公交撞大树了,这时小明打滴滴回家。
1 | public class Exceptionally { |
1 | 1624811127907-1-main-小明走出餐厅, 来到公交站 |
exceptionally
就是遇到异常之后,小明处理异常的步骤,exceptionally当然不只可以加在尾部还能在上面的链式中任何一步加。相比于try-catch
块来说真的简洁不少。
扩展
在上面我们学了6个API。
1 | # supplyAsync 用来开启异步任务 |
以上6个API大部分都满足下面三个规则:
- xxx(arg)
- xxxAsync(arg)
- xxxAsync(arg,Executor)
1 | CompletableFuture<String> invoice = CompletableFuture.supplyAsync(() -> { |
Apply
方法就是上一个任务执行完之后去执行下一个任务,supplyAsync
和thenApply
中的代码都在同一个线程里运行,thenApply
就是把后面的代码块放在上面同一个代码块里的去运行,CompletableFuture
会把这两个代码封装成一个任务去运行,随意这两个代码在一个线程里运行。当调用thenApplyAsync
方法CompletableFuture
就会把这两块代码看成是独立的两个任务。我们明白了thenApply
和thenApplyAsync
的区别,那另外的也是一样的。
由于CompletableFuture
存在线程的复用,我们不能只观察线程的ID来判断。
类似方法比较
1 | # supplyAsync 开启异步任务且提供返回值 |
1 | # thenApply 接收前面任务参数且有返回值 |
1 | # thenCombine 接收前面两个任务任务参数且有返回值 |
1 | # applyToEither 会得到最快执行完任务的结果,有返回值 |
1 | # exceptionally 处理前面任务的异常并且修正成一个正常值 |
性能问题
任务巨多,如何保证性能?
1 | public class TerribleCode { |
1 | 1624982786532-1-main-小明和小伙伴们 进餐厅点菜 |
由于制作一盆菜需要1S所以七盘菜需要7S,硬生生把多线程写成了单线程还不如直接放在main线程中直接执行七次。直接把异步变成了串行。
1 | public class TerribleCodeImprove { |
1 | 1624982458305-1-main-小明和小伙伴们 进餐厅点菜 |
这时我们发现七盘菜1S就做好了,那同时执行的线程能有多少个呢,我们只需要每次都多加一盘菜,什么时候发现执行的时间要之前的两倍就知道同时能执行多少个线程了。
1 | public static void main(String[] args) { |
1 | 1624982828176-1-main-小明和小伙伴们 进餐厅点菜 |
我们发现在第八盘菜的时候就需要2S了那这个8这个数从哪里来的呢,然后JDK开发人员就写死了8把,显然是不可能的。
1 | public class CommonPoolSize { |
这边我这台电脑是4核8线程的CPU,也就是说我这台电脑能同时运行八个任务,上面的7就是通过8-1得到的。如果你的处理器支持超线程的话就要用线程数去减了得到的结果就是CompletableFuture
底层用到的线程池的最大线程数。
我们想在知道只要任务大于7就是大量任务了,假如我们项目中很多时候任务数量都是大于7的,那我们只要修改线程池的最大线程数不久好了吗,我们只需要在线程池初始化之前改掉最大线程数就好了。
1 | public class GoodCode { |
1 | 1624983453369-1-main-小明和小伙伴们 进餐厅点菜 |
我们发现现在8盘菜只需要1S就做好了,把线程数设置一个合适的数值就能调整性能了。这个CommonPool不单单给CF使用,所以我们在开发中需要创建一个自定义的线程池来给CF使用。方便控制且互不影响。
我们也可以根据任务数量动态创建自定义线程池,当任务来了就创建线程池,当执行完之后再把线程池给销毁。
1 | public class CustomThreadPool { |
1 | 1624983798696-101-pool-1-thread-91-菜90制作完毕,来吃我把 |
我们发现创建100到菜也只需要1S,因为newCachedThreadPool
可是个大胃王,他能不停的创建线程
1 | public static ExecutorService newCachedThreadPool() { |
如何观察任务调度的情况?
1 | /** |
1 | 1624984232215-11-ForkJoinPool.commonPool-worker-1-A |
由于A很快就结束了,B一开始就发现有空闲线程,就直接复用A的线程了。也就是A和B在同一个线程中执行。
1 | public class DealThenRunAsyncThreadReuse { |
1 | 1624984512899-11-pool-1-thread-1-A |
这样我们发现A和B永远不会在一个线程中运行,注意这种方式只能用于测试和研究不要写到生产环境去。