Callable、Future、FutureTask详解

350 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情

一、三者之间的关系

Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接 口的类与实现Runnable的类都是可以被线程执行的任务。

三者之间的关系:

  1. Callable是Runnable封装的异步运算任务。
  2. Future用来保存Callable异步运算的结果
  3. FutureTask封装Future的实体类

1、Callable 与 Runnbale的区别

  • Callable定义的方法是call,而Runnable定义的方法是run。
  • call方法有返回值,而run方法是没有返回值的。
  • call方法可以抛出异常,而run方法不能抛出异常。

2、Future

Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结果

3、FutureTask

可取消的异步计算,此类提供了对Future的基本实现,仅在计算完成时才能获取结果,如果计算尚未完成,则阻塞get方法。

FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。

4、Callable 与 FutureTask

定义一个callable的任务:

public class MyCallableTask implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("callable do something");
        Thread.sleep(5000);
        return new Random().nextInt(100);
    }
    
    public static void main(String[] args) throws Exception {
        Callable<Integer> callable = new MyCallableTask();
        FutureTask<Integer> future = new FutureTask<>(callable);
        Thread thread = new Thread(future);
        thread.start();
        Thread.sleep(100);
        //尝试取消对此任务的执行
        future.cancel(true);
        //判断是否在任务正常完成前取消
        System.out.println("future is cancel:" + future.isCancelled());
        if (!future.isCancelled()) {
            System.out.println("future is cancelled");
        }
        //判断任务是否已完成
        System.out.println("future is done:" + future.isDone());
        if (!future.isDone()) {
            System.out.println("future get=" + future.get());
        } else {
            //任务已完成
            System.out.println("task is done");
        }
    }
}

执行结果:

image.png

这个DEMO主要是通过调用FutureTask的状态设置的方法,演示了状态的变迁。

  • 尝试取消对任务的执行,该方法如果由于任务已完成、已取消则返回false,如果能够取消还未完成的任务,则返回true,该DEMO中由于任务还在休眠状态,所以可以取消成功。

  • 判断任务取消是否成功:如果在任务正常完成前将其取消,则返回true

  • 判断任务是否完成:如果任务完成,则返回true,以下几种情况都属于任务完成:正常终止、异常或者取消而完成。DEMO中,任务是由于取消而导致完成。

  • 获取异步线程执行的结果,我这个DEMO中没有执行到这里,需要注意的是,future.get方法会阻塞当前线程, 直到任务执行完成返回结果为止。

二、Callable 与 Future

public class CallableThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("进入Call方法,开始休眠,休眠时间为:" + System.currentTimeMillis());
        Thread.sleep(10000);
        return "今天停电";
    }

    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable<String> call = new CallableThread();
        Future<String> fu = es.submit(call);
        es.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程休眠5秒,当前时间" + System.currentTimeMillis());
        String str = fu.get();
        System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" + System.currentTimeMillis());
    }
}

执行结果:

image.png

这里的future是直接扔到线程池里面去执行的。由于要打印任务的执行结果,所以从执行结果来看,主 线程虽然休眠了5s,但是从Call方法执行到拿到任务的结果,这中间的时间差正好是10s,说明get方法 会阻塞当前线程直到任务完成。

通过FutureTask也可以达到同样的效果

public static void main(String[] args) throws Exception {
    ExecutorService es = Executors.newSingleThreadExecutor();
    Callable<String> call = new CallableThread();
    FutureTask<String> task = new FutureTask<String>(call);
    es.submit(task);
    es.shutdown();
    Thread.sleep(5000);
    System.out.println("主线程等待5秒,当前时间为:" + System.currentTimeMillis());
    String str = task.get();
    System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" + System.currentTimeMillis());
}

以上的组合可以给我们带来这样的一些变化:

如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪费,如果采用前面两种组合,则效率会提高:

  1. 先把A方法的内容放到Callable实现类的call()方法中

  2. 在主线程中通过线程池执行A任务

  3. 执行后面方法中10秒不依赖方法A运行结果的代码

  4. 获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码

这样代码执行效率一下子就提高了,程序不必卡在A方法处。

三、总结

本篇主要介绍了 Callable、Future、FutureTask的关系与区别,通过案例分析,如果想要异步执行待返回结果的话,可以通过 Future 获取,如果不关心异步执行任务的结果,则直接 Runnable 实现。