由CompletableFuture cancel 引发的一系列问题track
由CompletableFuture cancel 引发的一系列问题track
一、背景
最近接手了一个别人正在维护的工程项目,后续需要我来负责开发维护,其中的一个服务,核心流程大概是从数据库三个表中串行查询三类数据,每一个表查询一类数据,然后组合成一个结果返回出去。由于之前组里的代码多次用到了异步线程的地方,所以直接用CompletableFuture对上述流程进行改造,CompletableFuture是一种组合式异步编程API,由于是多核服务器,最终必然可以提高接口的响应速度。示意图如下,可以利用多核cpu的特点,充分提高任务的处理速度。

二、CompletableFuture是什么?
简单粗暴,直接先看源码注释

大概意思是可以显式完成(设置其值和状态)并可用作 CompletionStage 的 Future,支持在其完成时触发的相关功能和操作。ok,看到这里就需要了解Future和CompletionStage 是什么。
2.1Future是什么?
首先看Future的注释:

大概意思是说Future 表示异步计算的结果。 提供了检查计算是否完成、等待其完成以及检索计算结果的方法。 结果只能在计算完成时使用 get 方法获取,必要时会阻塞直到准备就绪。 取消是通过 cancel 方法执行的。 提供了其他方法来确定任务是正常完成还是被取消。 一旦计算完成,就不能取消计算。 如果您想为了可取消性而使用 Future 但不提供可用的结果,您可以声明 Future<?> 形式的类型并返回 null 作为底层任务的结果。
2.2 CompletionStage 是什么?
首先看CompletionStage 的注释:

大概意思是说可能是异步计算的一个阶段,它在另一个 CompletionStage 完成时执行操作或计算值。 一个阶段在其计算终止时完成,但这可能反过来触发其他相关阶段。 此接口中定义的功能仅采用几种基本形式,这些形式扩展为更大的方法集以捕获一系列使用方式,
2.3 总结
通过看上述注释及其里面的代码可知:
- Future与CompletionStage 都是一个接口,里面包含了许多接口方法
- Future 代表异步计算的结果,CompletionStage 代表异步计算的一个阶段
- CompletableFuture 在JDK1.8才有,它可以支持在其完成时触发的相关功能和操作,也是一种CompletionStage 的Future
- Future与CompletableFuture 有什么区别呢?或者说CompletableFuture 的出现是为了解决什么原来仅使用Future的什么问题呢?
- Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致臭名昭著的回调地狱
- CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题
三、例子
3.1 Future
public void test() throws ExecutionException, InterruptedException, TimeoutException {
String response = "";
//创建了一个线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutorBuilder()
.coreSize(100)
.maxSize(100)
.queueSize(100 * 5)
.daemon(true)
.threadName("threadPool")
.build();
//创建一个Future集合
List<Future> futureList = new ArrayList<>();
//将thread1这个函数任务交给线程池去处理,submit后线程池就使用子线程去处理thread1这个函数了,同时主线程会继续往下执行,这就是异步的作用
Future<String> future1 = threadPool.submit(() -> thread1());
//同理
Future<String> future2 = threadPool.submit(() -> thread2());
Future<String> future3 = threadPool.submit(() -> thread3());
futureList.add(future1);
futureList.add(future2);
futureList.add(future3);
//这里一般设置等待时间,来获取子线程的结果,如果不设置时间,主线程会阻塞一直等待子线程运行结束,在微服务的应用下,一般是不可能接收这类场景的
for (Future future : futureList) {
String result= (String) future.get(2000, TimeUnit.MILLISECONDS);
System.out.println("result="+result);
response = response + result+",";
}
System.out.println("response=" + response);
}
public String thread1() {
System.out.println("thread1:" + Thread.currentThread().getName());
return "这是数据库表1的数据";
}
public String thread2() {
System.out.println("thread2:" + Thread.currentThread().getName());
return "这是数据库表2的数据";
}
public String thread3() {
System.out.println("thread3:" + Thread.currentThread().getName());
return "这是数据库表3的数据";
}
-----------输出-----------------
thread1:threadPool-0
thread2:threadPool-1
result=这是数据库表1的数据
result=这是数据库表2的数据
thread3:threadPool-2
result=这是数据库表3的数据
response=这是数据库表1的数据,这是数据库表2的数据,这是数据库表3的数据,
3.1 CompletableFuture
public class Controller {
private static String response = "";
public void testCompletableFuture() throws ExecutionException, InterruptedException, TimeoutException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutorBuilder()
.coreSize(100)
.maxSize(100)
.queueSize(100 * 5)
.daemon(true)
.threadName("threadPool")
.build();
//创建一个CompletableFuture列表
List<CompletableFuture> futureList = new ArrayList<>();
//supplyAsync 代表 以一个有返回值、异步的形式,把thread1这个任务交由threadPool线程池异步处理,当thread1这个任务处理完后执行whenComplete中的merge函数,对结果进行合并
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(this::thread1, threadPool).whenComplete((result, e) -> merge(result));
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(this::thread2, threadPool).whenComplete((result, e) -> merge(result));
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(this::thread3, threadPool).whenComplete((result, e) -> merge(result));
futureList.add(future1);
futureList.add(future2);
futureList.add(future3);
//allOf 代表当需要多个任务全部完成时使用allOf
CompletableFuture<Void> completableFuture = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{}));
//主线程等待1s
completableFuture.get(1000, TimeUnit.MILLISECONDS);
}
}
public void merge(String resultUnit) {
response = response + resultUnit + ",";
}
---------输出--------------
thread1:threadPool-0
thread2:threadPool-1
thread3:threadPool-2
response=这是数据库表1的数据,这是数据库表2的数据,这是数据库表3的数据,
在CompletableFuture里面我们可以发现, CompletableFuture.allOf是没有返回值的,如果我们想要得到每个任务的返回该怎么处理呢?这就是CompletableFuture诞生的主要原因,他会有一系列回调函数去处理,避免了Future手动的写回调逻辑。
四、任务取消
如果某一个子线程的任务耗时不是很稳定,而主线程设置了超时时间,一旦过了超时时间,子线程就不再继续执行下去了。针对这种场景该怎么做呢?我们改造下代码。在Thread3函数里面使线程sleep5秒,同时尝试取消线程的执行
public String thread3() throws InterruptedException {
Thread.sleep(5000);
System.out.println("thread3:" + Thread.currentThread().getName());
return "这是数据库表3的数据";
}
public void test() throws ExecutionException, InterruptedException, TimeoutException {
String response = "";
ThreadPoolExecutor threadPool = new ThreadPoolExecutorBuilder()
.coreSize(100)
.maxSize(100)
.queueSize(100 * 5)
.daemon(true)
.threadName("threadPool")
.build();
List<Future> futureList = new ArrayList<>();
Future<String> future1 = threadPool.submit(() -> thread1());
Future<String> future2 = threadPool.submit(() -> thread2());
Future<String> future3 = threadPool.submit(() -> thread3());
futureList.add(future1);
futureList.add(future2);
futureList.add(future3);
for (Future future : futureList) {
String result = "";
try {
result = (String) future.get(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
System.out.println(e);
future.cancel(true);//超时就可以捕获到异常,然后尝试取消这个future的任务
}
System.out.println("result=" + result);
response = response + result + ",";
}
System.out.println("response=" + response);
}
public String thread3() throws InterruptedException {
timeSpend();
System.out.println("thread3:" + Thread.currentThread().getName());
return "这是数据库表3的数据";
}
//模拟耗时
public void timeSpend() {
for (int i = 0; i < 9999; i++) {
for (int j = 0; j > -999; j--) {
System.out.println("thread3 is running");
}
}
}
运行后发现 response=“这是数据库表1的数据,这是数据库表2的数据,,” 没有数据库表3的数据,但是控制台还在一直打印“thread3 is running”,这说明future.cancel(true)这句话是不起作用的;但是考虑这种情况,我把timeSpend()模拟耗时操作改成Thred.sleep(5000),让当前线程睡5秒看有什么现象。
public String thread3() throws InterruptedException {
Thread.sleep(5000);
System.out.println("thread3:" + Thread.currentThread().getName());
return "这是数据库表3的数据";
}
-------------输出-------------
thread1:threadPool-0
result=这是数据库表1的数据
thread2:threadPool-1
result=这是数据库表2的数据
java.util.concurrent.TimeoutException
result=
response=这是数据库表1的数据,这是数据库表2的数据,,
这次竟然future.cancel(true)这句话是起作用的,进这个函数看一下代码注释

大概意思是这个方法试图取消此任务的执行。如果任务已经完成、已经取消或由于其他原因无法取消,则尝试将失败。如果取消成功了,并且在调用cancel时此任务尚未启动,则此任务应永远不会运行。如果任务已经启动,那么mayInterruptIfRunning参数决定是否应该中断执行此任务的线程以试图停止该任务。
看来要进一步根据线程的状态来加以区分:
- 如果线程已经处于running状态了,此时是取消不掉的,除非在线程里面通过参数手动判断需不需要中断任务
- 如果线程任务已经执行结束了,那么取消是没有意义的,同样取消不掉
- 如果线程任务还没开始执行,或者处于阻塞、sleep状态,这时是可以取消掉的
针对第一种情况线程已经处于running状态了,需要手动判断
public void timeSpend() {
for (int i = 0; i < 9999; i++) {
for (int j = 0; j > -999; j--) {
//判断是否调用了future.cancel,如果调用了则执行
if(Thread.currentThread().isInterrupted()){
return;
}
System.out.println("thread3 is running");
}
}
}
此时发现thread3并不会一直执行下去了,因为触发了if条件
那么在CompletableFuture里面怎么取消正在执行的线程呢?我这里使用了一个线程安全的公共变量来实现的
private AtomicBoolean isFalse = new AtomicBoolean(false);
public void testCompletableFuture() throws ExecutionException, InterruptedException, TimeoutException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutorBuilder()
.coreSize(100)
.maxSize(100)
.queueSize(100 * 5)
.daemon(true)
.threadName("threadPool")
.build();
List<CompletableFuture> futureList = new ArrayList<>();
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(this::thread1, threadPool).whenComplete((result, e) -> merge(result));
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(this::thread2, threadPool).whenComplete((result, e) -> merge(result));
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
return thread3();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, threadPool).whenComplete((result, e) -> merge(result));
futureList.add(future1);
futureList.add(future2);
futureList.add(future3);
CompletableFuture<Void> completableFuture = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{}));
try {
completableFuture.get(1000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
completableFuture.cancel(true);
}
System.out.println("response=" + response);
}
public String thread3() throws InterruptedException {
timeSpend();
System.out.println("thread3:" + Thread.currentThread().getName());
return "这是数据库表3的数据";
}
public void timeSpend() {
for (int i = 0; i <= 9999; i++) {
for (int j = 0; j > -999; j--) {
if (isFalse.get()) {
return;
}
System.out.println("thread3 is running");
}
}
}
同样可以取消子线程的执行
参考文章:

浙公网安备 33010602011771号