Callable详解之《我call(),Future在哪里》

Callable详解之《我call(),Future在哪里》

​ 即使作为初学者,我们也知道在Java中创建线程主要有三种方式:1.继承Thread类;2.实现Runnable接口;3.实现Callable接口。而后两者的区别在于Callable接口中的call()方法可以异步地返回一个计算结果Future,并且一般需要配合ExecutorService来执行。这一套操作在代码实现上似乎也并不难,可是对于call()方法具体怎么(被ExecutorService)执行的,以及Future这个结果是怎么获取的,却又不是很清楚了。

​ 那么今天,我们就一起来学习下Callable接口以及Future的使用,主要面向两个问题:1.承载着具体任务的call()方法如何被执行的;2.任务的执行结果如何得到。你可能会说,这两个难道不是一个问题吗,任务执行了就会有返回结果,而返回结果也一定是任务执行了才返回的,难道还能返回一个其他任务的结果么??不要着急,耐心的看下去,你就会发现,这两个还真的就是一个问题。

​ 本文将分为两个部分,第一部分分别介绍“任务”、“执行”、以及“结果”这三个概念在Java API中的实体和各自的继承关系,第二部分通过一个简单的例子回顾他们的用法,再理解下这两个问题的答案。

一、Callable、Executor与Future

​ 既然是一个任务被执行并返回结果,那么我们先来看看具体的任务,也就是Callable接口。

1.1 任务:Callable

​ 非常简单,只包含一个有泛型返回值的call()方法,需要在最后返回定义类型的结果。如果任务没有需要返回的结果,那么将泛型V设为Void并return null;就可以了。对比的是Runnable,另一个明显的区别则是Callable可以抛出异常

public interface Callable<V> {
    V call() throws Exception;
}


public interface Runnable {
    public abstract void run();
}

1.2 执行:ExecutorService

​ 说到线程就少不了线程池,而说到线程池肯定离不开Executor接口。下面这幅图是Executor的框架,我们常用的是其中的两个具体实现类ThreadPoolExecutor以及ScheduledThreadPoolExecutor,在Executors类中通过静态方法获取。Executors中包含了线程池以及线程工厂的构造,与Executor接口的关系类似于Collection接口和Collections类的关系。

Executor结构

​ 那么我们自顶向下,从源码上了解一下Executor框架,学习学习任务是如何被执行的。首先是Executor接口,其中只定义了execute()方法。

public interface Executor {
    void execute(Runnable command);
}

​ ExecutorService接口继承了Executor接口,主要扩展了一系列的submit()方法以及对executor的终止和判断状态。以第一个<T> Future<T> submit(Callable<T> task);为例,其中task为用户定义的执行的异步任务,Future表示了任务的执行结果,泛型T代表任务结果的类型。

public interface ExecutorService extends Executor {

    void shutdown();                // 现有任务完成后停止线程池
 
    List<Runnable> shutdownNow();   // 立即停止线程池

    boolean isShutdown();           // 判断是否已停止

    boolean isTerminated();

    <T> Future<T> submit(Callable<T> task);        // 提交Callale任务

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    // 针对Callable集合的invokeAll()等方法
}

​ 抽象类AbstractExecutorService是ThreadPoolExecutor的基类,在下面的代码中,它实现了ExecutorService接口中的submit()方法。注释中是对应的newTaskFor()方法的代码,非常简单,就是将传入的Callable或Runnable参数封装成一个FutureTask对象。

    // 1.第一个重载方法,参数为Callable
	public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        // return new FutureTask<T>(callable);
        execute(ftask);
        return ftask;
    }

    // 2.第二个重载方法,参数为Runnable
	public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        // return new FutureTask<T>(task, null);
        execute(ftask);
        return ftask;
    }

	// 3.第三个重载方法,参数为Runnable + 返回对象
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        // return new FutureTask<T>(task, result);
        execute(ftask);
        return ftask;
    }

​ 那么也就是说,无论传入的是Callable还是Runnable,submit()方法其实就做了三件事:

submit()
​ 具体的说,submit()中首先生成了一个RunnableFuture引用的FutureTask实例,然后调用execute()方法来执行它,那么我们可以推测FutureTask继承自RunnableFuture,而RunnableFuture又实现了Runnable,因为execute()的参数应为Runnable类型。上面还涉及到了FutureTask的构造函数,也来看一下。

    public FutureTask(Callable<V> callable) {
        this.callable = callable;
        this.state = NEW;
    }

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result); // 通过适配器将runnable在call()中执行并返回result
        this.state = NEW;
    }

​ FutureTask共有两个构造方法。第一个构造方法比较简单,对应上面的第一个submit(),采用组合的方式封装Callable并将状态设为NEW;而第二个构造方法对应上面的后两个submit()重载,不同之处是首先使用了Executors.callable来将Runnable和result组合成Callable,这里采用了适配器RunnableAdapter implements Callable,巧妙地在call()中执行Runnable并返回结果。

    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;                // 返回的结果;显然:需要在run()中赋值
        
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

​ 现在可以给出submit()方法执行的完整流程,总结一下就是Callable被封装在Runnable的子类FutureTask中传入execute()得以执行

submit()完整

1.3 结果:Future

​ 要说Future就是异步任务的执行结果其实并不准确,因为它代表了一个任务的执行过程,有状态、可以被取消,而get()方法的返回值才是任务的结果。不信?请看。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

​ 在1.2中还提到了RuunableFuture和FutureTask。从官方的注释来看,RuunableFuture就是一个可以run的future,实现了Runnable和Future两个接口,在run()方法中执行完计算时应该将结果保存起来以便通过get()获取。

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation unless it has been cancelled.
     */
    void run();
}

​ FutureTask直接实现了RunnableFuture接口,作为执行过程,共有下面这几种状态,其中COMPLETING为一个暂时状态,表示正在设置结果或异常,对应的,设置完成后状态变为NORMAL或EXCEPTIONAL;CANCELLED、INTERRUPTED表示任务被取消或中断。在上面的构造方法中,将state初始化为NEW。

    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

​ 然后是FutureTask的主要内容,主要是run()和get()。注意outcome的注释,无论是否发生异常返回的都是这个outcome,因为在执行中如果执行成功就将结果设置给了它(set()),而发生异常时将异常赋给了他(setException()),而在获取结果时也都返回了outcome(通过report())。

public class FutureTask<V> implements RunnableFuture<V> {
    
    private Callable<V> callable;         // target,待执行的任务
    
    /** 保存执行结果或异常,在get()方法中返回/抛出 */
    private Object outcome; // 非volatile,通过CAS保证线程安全
    
    
    public void run() {
        ......
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();            // 调用call()执行用户任务并获取结果
                ran = true;                   // 执行完成,ran置为true
            } catch (Throwable ex) {          // 调用call()出现异常,而run()方法继续执行
                 result = null;
                 ran = false;
                 setException(ex);            
                 // setException(Throwable t): compareAndSwapInt(NEW, COMPLETING);  outcome = t;      
            }
            if (ran)
                set(result);                  
            	// set(V v): compareAndSwapInt(NEW, COMPLETING);  outcome = v;
        }
    }
    
    
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);         // 加入队列等待COMPLETING完成,可响应超时、中断
        return report(s);
    }

    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        // 超时等待
    }
    
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)                              // 将outcome作为执行结果返回
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);   // 将outcome作为捕获的返回
    }
}

​ FutureTask实现了RunnableFuture接口,所以有两方面的作用。**第一,作为Runnable传入execute()方法来执行,同时封装Callable对象并在run()中调用其call()方法;第二,作为Future管理任务的执行状态,将call()的返回值保存在outcome中以通过get()获取。**这似乎就能回答开头的两个问题,并且浑然天成,就好像是一个问题,除非发生异常的时候返回的不是任务的结果而是异常对象。

​ 总结一下继承关系:
FutureTask继承关系

二、使用举例

​ 文章的标题有点唬人,说到底还是讲Callable的用法。现在我们知道了Future代表了任务执行的过程和结果,作为call()方法的返回值来获取执行结果;而FutureTask是一个Runnable的Future,既是任务执行的过程和结果,又是call方法最终执行的载体。下面通过一个例子看看他们在使用上的区别。

​ 首先创建一个任务,即定义一个任务类实现Callable接口,在call()方法里添加我们的操作,这里用耗时三秒然后返回100模拟计算过程。

class MyTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程开始计算...");
        for (int i=0;i<3;++i){
            Thread.sleep(1000);
            System.out.println("子线程计算中,用时 "+(i+1)+" 秒");
        }
        System.out.println("子线程计算完成,返回:100");
        return 100;
    }
}

​ 然后呢,创建一个线程池,并实例化一个MyTask备用。

ExecutorService executor = Executors.newCachedThreadPool();
MyTask task = new MyTask();

​ 现在,分别使用Future和FutureTask来获取执行结果,看看他们有什么区别。

2.1 使用Future

​ Future一般作为submit()的返回值使用,并在主线程中以阻塞的方式获取异步任务的执行结果。

        try {
            System.out.println("主线程启动线程池");
            Future<Integer> future = executor.submit(task);
            System.out.println("主线程得到返回结果:"+future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

​ 看看输出结果:

主线程启动线程池
子线程开始计算...
子线程计算中,用时 1 秒
子线程计算中,用时 2 秒
子线程计算中,用时 3 秒
子线程计算完成,返回:100
主线程得到返回结果:100

​ 由于get()方法阻塞获取结果,所以输出顺序为子线程计算完成后主线程输出结果。

2.2 使用FutureTask

​ 由于FutureTask集“任务”与“结果”于一身,所以我们可以使用FutureTask自身而非返回值来管理任务,这需要首先利用Callable对象来构造FutureTask,并调用不同的submit()重载方法。

        try {
            System.out.println("主线程启动线程池");
            FutureTask<Integer> futureTask = new FutureTask<>(task);
            executor.submit(futureTask);                                 // 作为Ruunable传入submit()中
            System.out.println("主线程得到返回结果:"+futureTask.get());    // 作为Future获取结果
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

​ 这段程序的输出与2.1中完全相同,其实两者在实际执行中的区别也不大,虽然前者调用了submit(Callable<T> task)而后者调用了submit(Runnable task),但最终都通过execute(futuretask)来把任务加入线程池中。

三、总结

​ 上面大费周章其实只是尽可能细致地讲清楚了Callable中的任务是如何执行的,总结起来就是:

  1. 线程池中,submit()方法实际上将Callable封装在FutureTask中,将其作为Runnable的子类传给execute()真正执行;

  2. FutureTask在run()中调用Callable对象的call()方法并接收返回值或捕获异常保存在Object outcome中,同时管理执行过程中的状态state

  3. FutureTask同时作为Future的子类,通过get()返回任务的执行结果,若未执行完成则通过等待队列进行阻塞等待完成;

    FutureTask作为一个Runnable的Future,其中最重要的两个方法如下。

run&get

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值