26 - Future 获取异步任务的结果

  在上一篇文章《25 - ThreadPoolExecutor 线程池》中,我们详细介绍了如何创建正确的线程池,也粗略了讲了一下如何启动线程池,execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。 submit() 方法可以配合 Futrue 来接收线程执行的返回值,下面我们就来详细看看到底该如何获取任务的执行结果。

  

1. 如何获取任务的执行结果

1.1 ThreadPoolExecutor.submit

  Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。下面我们先来介绍这 3 个 submit() 方法,这 3 个方法的方法签名如下:

// 提交Runnable任务
Future<?> submit(Runnable task);

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

// 提交Runnable任务及结果引用  
<T> Future<T> submit(Runnable task, T result);

  你会发现它们的返回值都是 Future 接口,Future 接口有 5 个方法,我都列在下面了,它们分别是取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone() 以及 2个获得任务执行结果的 get() 和 get(timeout, unit),其中最后一个 get(timeout, unit) 支持超时机制。通过 Future 接口的这 5 个方法你会发现,我们提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是:这两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。

// 取消任务
boolean cancel(boolean mayInterruptIfRunning);

// 判断任务是否已取消  
boolean isCancelled();

// 判断任务是否已结束
boolean isDone();

// 获得任务执行结果
get();

// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);

这 3 个 submit() 方法之间的区别在于方法参数不同,下面我们简要介绍一下:

  1. 提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join();
  2. 提交 Callable 任务 submit(Callable task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果;
  3. 提交 Runnable 任务及结果引用 submit(Runnable task, T result):这个方法很有意思,假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。这个方法该怎么用呢?下面这段示例代码展示了它的经典用法。需要你注意的是 Runnable 接口的实现类 Task 声明了一个有参构造函数 Task(Result r) ,创建 Task 对象的时候传入了 result 对象,这样就能在类 Task 的 run() 方法中对 result 进行各种操作了。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。
public class TestFuture {

    public static void main(String[] args) throws Exception {
        Result result = new Result();
        result.setId(10086L);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
                60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        Future<Result> resultFuture = threadPoolExecutor.submit(new Task(result), result);
        Result result1 = resultFuture.get();
        System.out.println(result == result1);
        System.out.println(result1.getId());
        System.out.println(result1.getDesc());
    }

    static class Task implements Runnable {
        private Result result;

        public Task(Result result) {
            this.result = result;
        }

        @Override
        public void run() {
            System.out.println("Result id is:" + result.getId());
            result.setDesc("这个一个 Future 的测试...");
        }
    }

    static class Result {
        private Long id;
        private String desc;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
}

# 运行结果如下:
Result id is:10086
true
10086
这个一个 Future 的测试...

  

1.2 FutureTask 工具类

  前面我们提到的 Future 是一个接口,而 FutureTask 是一个实实在在的工具类,这个工具类有两个构造函数,它们的参数和前面介绍的 submit() 方法类似。

FutureTask(Callable<V> callable);

FutureTask(Runnable runnable, V result);

  那如何使用 FutureTask 呢?其实很简单,FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。下面的示例代码是将 FutureTask 对象提交给 ThreadPoolExecutor 去执行。


// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);

// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();

// 提交FutureTask 
es.submit(futureTask);

// 获取计算结果
Integer result = futureTask.get();

  FutureTask 对象直接被 Thread 执行的示例代码如下所示。相信你已经发现了,利用 FutureTask 对象可以很容易获取子线程的执行结果。

// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);

// 创建并启动线程
Thread T1 = new Thread(futureTask);
T1.start();

// 获取计算结果
Integer result = futureTask.get();

  

2. 实现最优的“烧水泡茶”程序

  记得以前初中语文课文里有一篇著名数学家华罗庚先生的文章《统筹方法》,这篇文章里介绍了一个烧水泡茶的例子,文中提到最优的工序应该是下面这样:

在这里插入图片描述
  下面我们用程序来模拟一下这个最优工序。我们专栏前面曾经提到,并发编程可以总结为三个核心问题:分工、同步和互斥。编写并发程序,首先要做的就是分工,所谓分工指的是如何高效地拆解任务并分配给线程。对于烧水泡茶这个程序,一种最优的分工方案可以是下图所示的这样:用两个线程 T1 和 T2 来完成烧水泡茶程序,T1 负责洗水壶、烧开水、泡茶这三道工序,T2 负责洗茶壶、洗茶杯、拿茶叶三道工序,其中 T1 在执行泡茶这道工序时需要等待 T2 完成拿茶叶的工序。对于 T1 的这个等待动作,你应该可以想出很多种办法,例如 Thread.join()、CountDownLatch,甚至阻塞队列都可以解决,不过今天我们用 Future 特性来实现。

在这里插入图片描述
  下面的示例代码就是用这一章提到的 Future 特性来实现的。首先,我们创建了两个 FutureTask——ft1 和 ft2,ft1 完成洗水壶、烧开水、泡茶的任务,ft2 完成洗茶壶、洗茶杯、拿茶叶的任务;这里需要注意的是 ft1 这个任务在执行泡茶任务前,需要等待 ft2 把茶叶拿来,所以 ft1 内部需要引用 ft2,并在执行泡茶之前,调用 ft2 的 get() 方法实现等待。

public class TestFutureTask {

    public static void main(String[] args) throws Exception {
        FutureTask<String> ft1 = new FutureTask<>(new Task1());
        FutureTask<String> ft2 = new FutureTask<>(new Task2(ft1));
        Thread t1 = new Thread(ft1);
        t1.start();
        Thread t2 = new Thread(ft2);
        t2.start();
        System.out.println(ft2.get());
    }

    static class Task2 implements Callable<String> {
        private FutureTask<String> futureTask;

        public Task2(FutureTask futureTask) {
            this.futureTask = futureTask;
        }

        @Override
        public String call() throws Exception {
            System.out.println("线程2:吸水壶");
            Thread.sleep(1000);
            System.out.println("线程2:烧开水");
            Thread.sleep(1000);
            String result = futureTask.get();
            System.out.println("线程2:拿到茶叶:" + result);
            System.out.println("线程2:泡茶");
            Thread.sleep(1000);
            return "上茶喽...";
        }
    }

    static class Task1 implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("线程1:洗茶壶");
            Thread.sleep(1000);
            System.out.println("线程1:洗茶杯");
            Thread.sleep(1000);
            System.out.println("线程1:拿茶叶");
            Thread.sleep(1000);
            return "龙井";
        }
    }
}

# 运行结果如下:
线程1:洗茶壶
线程2:吸水壶
线程1:洗茶杯
线程2:烧开水
线程1:拿茶叶
线程2:拿到茶叶:龙井
线程2:泡茶
上茶喽...

  

3. 总结

  利用 Java 并发包提供的 Future 可以很容易获得异步任务的执行结果,无论异步任务是通过线程池 ThreadPoolExecutor 执行的,还是通过手工创建子线程来执行的。Future 可以类比为现实世界里的提货单,比如去蛋糕店订生日蛋糕,蛋糕店都是先给你一张提货单,你拿到提货单之后,没有必要一直在店里等着,可以先去干点其他事,比如看场电影;等看完电影后,基本上蛋糕也做好了,然后你就可以凭提货单领蛋糕了。

  利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用 Future 来解决。在分析这种问题的过程中,建议你用有向图描述一下任务之间的依赖关系,同时将线程的分工也做好,类似于烧水泡茶最优分工方案那幅图。对照图来写代码,好处是更形象,且不易出错。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值