由CompletableFuture cancel 引发的一系列问题track

由CompletableFuture cancel 引发的一系列问题track

一、背景

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

二、CompletableFuture是什么?

简单粗暴,直接先看源码注释

image-20230116154025915

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

2.1Future是什么?

首先看Future的注释:

image-20230116154226948

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

2.2 CompletionStage 是什么?

首先看CompletionStage 的注释:

image-20230116154413181

大概意思是说可能是异步计算的一个阶段,它在另一个 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)这句话是起作用的,进这个函数看一下代码注释

image-20230116174907946

大概意思是这个方法试图取消此任务的执行。如果任务已经完成、已经取消或由于其他原因无法取消,则尝试将失败。如果取消成功了,并且在调用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");
            }
        }
    }

同样可以取消子线程的执行

参考文章:

posted @ 2023-01-17 16:20  Dream可乐  阅读(154)  评论(0)    收藏  举报