带你玩转CompletableFuture异步编程
前言
最近在忙生活的第一个OKR,这个等等后面具体聊聊,今天开始恢复每周一篇原创,感谢小伙伴的不离不弃。这篇文章也是最近在Code Review的时候,看到的大家代码,想整体推下大家异步编程的思想,由此而写的。
为什么使用CompletableFuture
一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。 JDK5新增了Future接口,用于描述一个异步计算的结果。虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,我们必须使用Future.get的方式阻塞调用线程,或者使用轮询方式判断 Future.isDone 任务是否结束,再获取结果。这两种处理方式都不是很优雅,相关代码如下:
@Test
public void testFuture() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<String> future = executorService.submit(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
});
try {
System.out.println(future.get());
System.out.println("end");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
此外就是无法解决多个异步任务需要相互依赖的场景,简单点说就是,主线程需要等待子线程任务执行完毕之后在进行执行,这个时候你可能想到了CountDownLatch,没错确实可以解决,代码如下,但是Java8以后我不在认为这是一种优雅的解决方式,接下来我们来了解下CompletableFuture的使用。
@Test
public void testCountDownLatch() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
CountDownLatch downLatch = new CountDownLatch(2);
Future<String> orderFuture = executorService.submit(() -> {
OrderService orderService = new OrderServiceImpl();
String result = orderService.queryOrderInfo();
downLatch.countDown();
return result;
});
Future<String> trailFuture = executorService.submit(() -> {
TrailService trailService = new TrailServiceImpl();
String result = trailService.queryTrail();
downLatch.countDown();
return result;
});
try {
downLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(orderFuture.get() + trailFuture.get());
System.out.println("end");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
使用介绍
创建实例
关于CompletableFuture的创建提供了5种方式,第一个就是创建一个具有默认结果的 CompletableFuture,不经常使用,我们常用就是runAsync和supplyAsync,重点关注下这两个静态方法。runAsync是不具有返回值,supplyAsync具有返回值,关于这两个方法都提供一个两种创建形式,一种是默认的使用公共的 ForkJoinPool 线程池执行,这个线程池默认线程数是 CPU 的核数。
另外一种使用就是提供一个主动创建线程池,这样做相比于ForkJoinPool的好处是,可以通过自定义不同场景的线程池,来进行业务划分方便发现问题,还有一个好处就是对于ForkJoinPool这种共享线程来说,一旦阻塞,会照成其他线程无法获取执行机会。
CompletionStage
关于CompletableFuture核心能力就是通过继承Future和CompletionStage来实现的,关于Future就是提供一些异步的能力,如果单存就这样CompletableFuture也就不会那么强大,所以我们核心就是介绍CompletionStage内部常用一些方法。
关于CompletionStage的方法,可以分为两种类型的接口,其中核心方法都提供异步的方式,这里关于异步的方法不进行介绍,基本上原理类似,都是新提供一个线程池去实现任务。
异步回调
关于异步回调可以分为两类,一种是有参数的返回,另外一种是无参数的返回,有参数返回的包括thenApply和thenCompose,无参数返回的包括thenRun和thenAccept。
有参数返回
thenApply 和 thenCompose表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中,也可以理解为串行化的,唯一不同的是thenCompose需要返回一个新的 CompletionStage,整体的使用如下:
@Test
public void testCompletableFuture() {
long start = System.currentTimeMillis();
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "aaaaaaaaaaaaaaaaaaaaa";
}).thenApply(x -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return x + "bbbbbbbbbbbbbbbbbbbbbbbb";
}).thenCompose(x -> CompletableFuture.supplyAsync(x::toUpperCase));
System.out.println(first.join());
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start) / 1000 + "");
}
无参数返回
thenAccep也是消费上一个任务的动作,将该任务的执行结果即方法返回值作为入参传递到回调方法中,只是无返回值,thenAccep与thenRun方法不同就是没有入参也没有返回值。
@Test
public void testCompletableFuture() {
long start = System.currentTimeMillis();
CompletableFuture<Void> first = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "a";
}).thenAccept(x -> {
System.out.println(x);
});
first.join();
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start) / 1000 + "秒");
}
异常
CompletableFuture方法执行过程若产生异常,只有get或者join方法的才能获取到异常,针对这种情况CompletableFuture提供三种处理异常的方式。
exceptionally
exceptionally的使用方式类似于 try catch中的catch代码块中异常处理。exceptionally当某个任务执行异常时执行的回调方法,将抛出的异常作为参数传递到回调方法中,如果该任务正常执行,exceptionally方法返回的CompletionStage的result就是该任务正常执行的结果。
@Test
public void testCompletableFuture() {
long start = System.currentTimeMillis();
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException("test");
});
CompletableFuture<String> two = first.exceptionally((x) -> {
x.printStackTrace();
return "123";
});
two.join();
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start) / 1000 + "秒");
}
whenComplete
whenComplete的使用类似于 try..catch..finanlly 中 finally 代码块,无论是否发生异常,都将会执行的。whenComplete当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法,如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致,如果该任务正常执行,则get方法返回执行结果,如果是执行异常,则get方法抛出异常。
@Test
public void testCompletableFuture() {
long start = System.currentTimeMillis();
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "aa";
}).whenComplete((x, throwable) -> {
// 如果异常存在,打印异常,并且返回默认值
if (throwable != null) {
throwable.printStackTrace();
System.out.println("失败");
} else {
System.out.println("成功");
}
});
System.out.println(first.join());
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start) / 1000 + "秒");
}
handle
跟whenComplete基本一致,区别在于handle的回调方法有返回值,且handle方法返回的CompletableFuture的result是回调方法的执行结果或者回调方法执行期间抛出的异常,与原始CompletableFuture的result无关。
@Test
public void testCompletableFuture() {
long start = System.currentTimeMillis();
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException("test");
}).handle((x, throwable) -> {
// 如果异常存在,打印异常,并且返回默认值
if (throwable != null) {
throwable.printStackTrace();
return "异常";
} else {
return x + "aa";
}
});
System.out.println(first.join());
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start) / 1000 + "秒");
}
组合关系
组合关系关系分为两种,一种是和的关系,一种是或的关系,和关系就是当所有的任务执行完成以后再继续执行,类似于CountDownLatch,或关系就是只要有一个任务执行完成以后就可以向下执行。
和关系
thenCombine /thenAcceptBoth / runAfterBoth/allOf
这四个方法可以将多个CompletableFuture组合起来,将多个CompletableFuture都执行完成以后,才能执行后面的操作,区别在于,thenCombine会将任务的执行结果作为方法入参传递到指定方法中,且该方法有返回值;thenAcceptBoth同样将任务的执行结果作为方法入参,但是无返回值;runAfterBoth没有入参,也没有返回值。注意多个任务中只要有一个执行异常,则将该异常信息作为指定任务的执行结果。allOf是多个任务都执行完成后才会执行,只要有一个任务执行异常,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回null。
@Test
public void testCompletableFuture() {
CompletableFuture<String> order = CompletableFuture.supplyAsync(() -> {
OrderService orderService = new OrderServiceImpl();
return orderService.queryOrderInfo();
});
CompletableFuture<String> trail = CompletableFuture.supplyAsync(() -> {
TrailService trailService = new TrailServiceImpl();
return trailService.queryTrail();
});
CompletableFuture<String> future = order.thenCombine(trail, (a, b) -> a + b);
CompletableFuture<Void> afterBoth = future.runAfterBoth(trail, () -> {
System.out.println(future.join());
});
CompletableFuture<Void> result = CompletableFuture.allOf(afterBoth);
try {
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
applyToEither / acceptEither / runAfterEither/anyOf
这四个方法可以将多个CompletableFuture组合起来,只需要其中一个CompletableFuture执行完成以后,就能执行后面的操作,applyToEither会将已经执行完成的任务的执行结果作为方法入参,并有返回值;acceptEither同样将已经执行完成的任务的执行结果作为方法入参,但是没有返回值;runAfterEither没有方法入参,也没有返回值,注意多个任务中只要有一个执行异常,则将该异常信息作为指定任务的执行结果。anyOf多个任务只要有一个任务执行完成,后续任务就可执行。
@Test
public void testCompletableFuture() {
CompletableFuture<String> order = CompletableFuture.supplyAsync(() -> {
OrderService orderService = new OrderServiceImpl();
return orderService.queryOrderInfo();
});
CompletableFuture<String> trail = CompletableFuture.supplyAsync(() -> {
TrailService trailService = new TrailServiceImpl();
return trailService.queryTrail();
});
CompletableFuture<String> future = order.applyToEither(trail, (result) -> result);
CompletableFuture<Void> afterBoth = future.runAfterEither(trail, () -> {
System.out.println(future.join());
});
CompletableFuture<Object> result = CompletableFuture.anyOf(afterBoth,order);
try {
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
项目中如何使用
CompletableFuture在自定义线程时,默认使用的线程池是 ForkJoinPool.commonPool(),对于我们用java常做的IO密集型任务,默认线程池是远远不够使用的;在双核及以下机器上,默认线程池又会退化为为每个任务创建一个线程,相当于没有线程池。因此对于CompletableFuture在项目中的使用一定要自定义线程池,同时又要注意自定义线程池,线程池有个容量满了的拒绝策略,如果采用丢弃策略的拒绝策略,并且allOf方法和get方法如果没有设置超时则会无限期的等待下去,接下来我们通过自定义线程使用CompletableFuture。
自定义线程池,此处通过继承ThreadPoolExecutor,重写了shutdown() 、shutdownNow() 、beforeExecute() 和 afterExecute()方法来统计线程池的执行情况,此处还可以结合Spring和appllo实现自定义扩展线程池,下一篇可以聊聊扩展思路以及实现方案,不同对的业务场景使用的不同的线程池,一是方便出现问题的排查,另外就是类似于Hystrix隔离的方案;
package com.zto.lbd;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程池监控类
*
* @author wangtongzhou 18635604249
* @since 2022-02-23 07:27
*/
public class ThreadPoolMonitor extends ThreadPoolExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolMonitor.class);
/**
* 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
*/
private ConcurrentHashMap<String, Date> startTimes;
/**
* 线程池名称,一般以业务名称命名,方便区分
*/
private String poolName;
/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize 线程池核心线程数
* @param maximumPoolSize 线程池最大线程数
* @param keepAliveTime 线程的最大空闲时间
* @param unit 空闲时间的单位
* @param workQueue 保存被提交任务的队列
* @param poolName 线程池名称
*/
public ThreadPoolMonitor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, String poolName) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), poolName);
}
/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize 线程池核心线程数
* @param maximumPoolSize 线程池最大线程数
* @param keepAliveTime 线程的最大空闲时间
* @param unit 空闲时间的单位
* @param workQueue 保存被提交任务的队列
* @param threadFactory 线程工厂
* @param poolName 线程池名称
*/
public ThreadPoolMonitor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, String poolName) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
this.startTimes = new ConcurrentHashMap<>();
this.poolName = poolName;
}
/**
* 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
*/
@Override
public void shutdown() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info("{} 关闭线程池, 已执行任务: {}, 正在执行任务: {}, 未执行任务数量: {}",
this.poolName, this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size());
super.shutdown();
}
/**
* 线程池立即关闭时,统计线程池情况
*/
@Override
public List<Runnable> shutdownNow() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info("{} 立即关闭线程池,已执行任务: {}, 正在执行任务: {}, 未执行任务数量: {}",
this.poolName, this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size());
return super.shutdownNow();
}
/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
// 已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、
// 最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
LOGGER.info("{}-pool-monitor: " +
"任务耗时: {} ms, 初始线程数: {}, 核心线程数: {}, 正在执行的任务数量: {}, " +
"已完成任务数量: {}, 任务总数: {}, 队列里任务数量: {}, 池中存在的最大线程数: {}, " +
"最大线程数: {}, 线程空闲时间: {}, 线程池是否关闭: {}, 线程池是否终止: {}",
this.poolName,
diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(),
this.getCompletedTaskCount(), this.getTaskCount(), this.getQueue().size(), this.getLargestPoolSize(),
this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS), this.isShutdown(), this.isTerminated());
}
/**
* 生成线程池所用的线程,改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪
*/
static class MonitorThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
/**
* 初始化线程工厂
*
* @param poolName 线程池名称
*/
MonitorThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = Objects.nonNull(s) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
}
使用自定义线程池的CompletableFuture;
private final static BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
private final static ThreadPoolMonitor threadPoolMonitor = new ThreadPoolMonitor(5, 10, 100L,
TimeUnit.SECONDS, workQueue, "monitor");
@Test
public void testCompletableFuture() {
CompletableFuture<String> order = CompletableFuture.supplyAsync(() -> {
OrderService orderService = new OrderServiceImpl();
return orderService.queryOrderInfo();
},threadPoolMonitor);
String result=order.join();
assertTrue(Objects.nonNull(result));
}
结束
欢迎大家点点关注,点点赞!
带你玩转CompletableFuture异步编程的更多相关文章
- 编程老司机带你玩转 CompletableFuture 异步编程
本文从实例出发,介绍 CompletableFuture 基本用法.不过讲的再多,不如亲自上手练习一下.所以建议各位小伙伴看完,上机练习一把,快速掌握 CompletableFuture. 个人博文地 ...
- Java8系列 (七) CompletableFuture异步编程
概述 Java8之前用 Future 处理异步请求, 当你需要获取任务结果时, 通常的做法是调用 get(long timeout, TimeUnit unit) 此方法会阻塞当前的线程, 如果任务 ...
- 异步编程利器:CompletableFuture
一.一个示例回顾Future 一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度. JDK5新增了Future接口,用于描述一个异步计算的结果.虽然 Future 以及相关使用方法提供了异步 ...
- 从CompletableFuture到异步编程设计
从CompletableFuture到异步编程设计,笔者就分为2部分来分享CompletableFuture异步编程设计,前半部分总结下CompletableFuture使用实践,后半部分分享下Com ...
- 异步编程CompletableFuture
多线程优化性能,串行操作并行化 串行操作 // 以下2个都是耗时操作 doBizA(); doBizB(); 修改变为并行化 new Thread(() -> doBizA()).start() ...
- 异步编程CompletableFuture实现高并发系统优化之请求合并
先说场景: 根据Redis官网介绍,单机版Redis的读写性能是12万/秒,批量处理可以达到70万/秒.不管是缓存或者是数据库,都有批量处理的功能.当我们的系统达到瓶颈的时候,我们考虑充分的压榨缓存和 ...
- 有了 CompletableFuture,使得异步编程没有那么难了!
本文导读: 业务需求场景介绍 技术设计方案思考 Future 设计模式实战 CompletableFuture 模式实战 CompletableFuture 生产建议 CompletableFutur ...
- 搞定 CompletableFuture,并发异步编程和编写串行程序还有什么区别?你们要的多图长文
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...
- SpringBoot 如何实现异步编程,老鸟们都这么玩的!
镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 首先我们来看看在Spring中为什么要使用异步编程,它能解决什么问题? 为什么要用异步框架,它解决什么问题? 在SpringBoot的日常开发中 ...
随机推荐
- Ubuntu16桌面版编译OpenCV4的java库和so库
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 【刷题-LeetCode】201 Bitwise AND of Numbers Range
Bitwise AND of Numbers Range Given a range [m, n] where 0 <= m <= n <= 2147483647, return t ...
- 微服务架构 | 2.2 Alibaba Nacos 的统一配置管理
目录 前言 1. Nacos 配置中心基础知识 1.1 Nacos 在配置中心中的功能 1.2 Nacos 配置管理 Data ID 的构成 1.3 Nacos 配置的回滚机制 1.4 Nacos 配 ...
- sqoop如何指定pg库的模式
摘要:sqoop如何指定pg库的模式? 本文分享自华为云社区<[Hadoop]关于Sqoop导出数据到postgresql时schema的设置问题>,作者:Copy工程师 . 说明 使用s ...
- 根据SVG Arc求出其开始角、摆动角和椭圆圆心
SVG Arc 目前Svg的Arc的参数字符串如下: a rx ry x-axis-rotation large-arc-flag sweep-flag x y 除了a表示标识为Arc之外,其余参数说 ...
- golang中自定义一些类型和对应类型的指针方法
package main import "fmt" // 项目开发中可以为type声明的类型编写一些方法,从而实现对象.方法的操作 // 声明类型 type myInt int / ...
- java多态instanceof介绍
1 public static void method(Animal a) {//类型判断 2 a.eat(); 3 if(a instanceof Cat) {//instanceof:用于判断对象 ...
- 多线程-线程间通信-多生产者多消费者问题(JDK1.5后Lock,Condition解决办法及开发中代码范例)
1 package multithread4; 2 3 import java.util.concurrent.locks.Condition; 4 import java.util.concurre ...
- xargs、管道、exec区别
作者:ilexwg链接:https://www.zhihu.com/question/27452459/answer/170834758来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转 ...
- IntelliJ IDEA 热部署,修改java文件 不用重启tomcat
详情见大佬:https://www.cnblogs.com/chenweichu/articles/6838842.html