解开Future的神秘面纱之取消任务

Posted longfurcat

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解开Future的神秘面纱之取消任务相关的知识,希望对你有一定的参考价值。

在之前写过的一篇随笔中已经提到了Future的应用场景和特性。(ExecutorService——<T> Future<T> submit(Callable<T> task)

我们先来回顾一下:

public class FutureCancelDemo {


    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        Future<Target> future = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2); //给足时间让启动起来,但又不足以让其完成
        future.cancel(true);
    }

}

class Target { //任务目标

}

class DemoTask implements Callable<Target> { //任务
    private static int counter = 0;
    private final int id = counter++;

    @Override
    public Target call() throws Exception {
        System.out.println(this+ " start...");
        TimeUnit.SECONDS.sleep(5); //模拟任务运行需要的时间
        System.out.println(this + " completed!");
        return new Target();
    }

    @Override
    public String toString() {
        return "Task[" + id + "]";
    }
}

 

一般情况下,我们会在哪里用到Future对象呢?

  就是当我们需要控制任务(Runnable/Callable对象)的时候,我们把任务提交给执行器(ExecutorService.submit()),并返回一个控制句柄(Future)。以便在未来的某个时刻检查任务执行状态、获取任务执行结果、以及在必要的时候取消任务等。

今天我们就来看看,Future是如何取消任务的。

 

我们知道Future只是一个接口,它到底是如何实现任务取消的呢?

  我们知道,把任务提交给执行器,执行器返回给我们一个Future。但是由于代码封装得很好,Future和ExecutorService都只是一个接口,我们只知道怎么用,却不知道其内部是如何实现的。如果要查看它到底是如何实现的就要追根溯源的查,直到找到最终的实现类。首先,我们的ExecutorService是用工厂类Executors获得的。就拿上述代码为例,我们获得了一个缓冲线程池ThreadPoolExecutor类型的对象,而ThreadPoolExecutor并没有重写submit方法,而是延用它父类的实现,而它父类便是AbstractExecutorSevice。这个类提供了ExecutorService接口最基本的底层实现。我们终于找到了submit方法的实现。

技术分享图片

从这里我们可以看到,该方法以RunnableFuture作为返回值。且该值由newTaskFor方法生成。

技术分享图片

然而这RunnableFuture,FutureTask,Future到底是何关系呢?

技术分享图片

即RunnableFuture继承了Runnable及Future接口,表示一个可控制的任务。而FutureTask实现了这个接口。故Future的取消操作最终由这个FutureTask实现。

我们来看看它是如何实现取消操作(future.cancel())的。

 技术分享图片

很明显,如果任务已经启动,则取消任务的方法就是中断执行它的线程

说到这里,可能有人会对cancel的参数mayInterruptIfRunning产生疑惑,这到底是用来干什么的。我们来看看,Future对该方法的定义。

技术分享图片

也就是说,如果mayInterruptIfRunning为true,则如果任务未启动,则修改任务状态标识,使得该任务无法启动。如果已经启动,则可以采用中断线程的策略结束任务。即未启动和已启动的任务都能取消。

如果mayInterruptIfRunning为false,则只能取消未启动的任务。已启动的任务会任由它继续执行。

 

通过一个例子加深一下印象:

public class FutureCancelDemo2 {


    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool(); //缓冲线程池

        Future<Target> future = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2); //给足时间让启动起来,但又不足以让其完成
        boolean cancelResult1 = future.cancel(true); //true表示,如果已经运行,则中断

        Future<Target> future2 = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2);
        boolean cancelResult2 = future2.cancel(false);

        System.out.println("cancelResult1:" + cancelResult1);
        System.out.println("cancelResult2:" + cancelResult2);

        try {
            Target target = future2.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

 

运行结果:

技术分享图片

对于上述代码及运行结果,是否感到奇怪。我来分享一下,我的疑惑之处吧。

①根据前面的定义,既然任务已经运行,那么cancel(false)应该不能中断任务才对,为何返回值会是true?

②由输出"Task[1] completed!"可知,任务2已经完成。为何future2.get()会报错?

解答:

我想当然的把cancel的返回值看作是取消的成功标志。我们来看一下Future接口的官方定义。

技术分享图片

也就是说,cancel的返回值如果是false,则情况有这几种:任务已经完成,在这之前已经调用过一次cancel,其他原因导致无法取消。

其他任何情况都会返回true

另外由于cancel(false)不会实际上中断正在运行的任务,但是会实现逻辑上的取消,即修改任务执行标识为"已取消"。故再调用get方法自然无意义。

我们来看看get操作的定义:

技术分享图片

 

以上是关于解开Future的神秘面纱之取消任务的主要内容,如果未能解决你的问题,请参考以下文章

解开Future的神秘面纱之获取结果

解开Redis的神秘面纱

解开Kafka神秘的面纱:kafka优雅应用

解开SQL注入的神秘面纱-来自于宋沄剑的分享

解开MongoDB神秘的面纱

解开Kafka神秘的面纱