四十六、Callable 和 Future

1、Callable 和 Runnable 的不同?

1.1 Runnable 接口

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

Runnable 是一个 interface,并且里面只有一个方法,叫作 public abstract void run()。这个方法已经规定了 run() 方法的返回类型是 void,而且这个方法没有声明抛出任何异常。所以,当实现并重写这个方法时,既不能改返回值类型,也不能更改对于异常抛出的描述,因为在实现方法的时候,语法规定是不允许对这些内容进行修改的。

所以Runnable 不能返回一个返回值,不能抛出 checked Exception。

为什么 Java 要把它设计成这个样子呢?
假设 run() 方法可以返回返回值,或者可以抛出异常,也无济于事,因为我们并没有办法在外层捕获并处理,这是因为调用 run() 方法的类(比如 Thread 类和线程池)是 Java 直接提供的,而不是我们编写的,所以就算它能有一个返回值,我们也很难把这个返回值利用到。

1.2 Callable 接口

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

可以看出它也是一个 interface,并且它的 call 方法中已经声明了 throws Exception,前面还有一个 V 泛型的返回值,这就和 Runnable 有很大的区别。实现 Callable 接口,就要实现 call 方法,这个方法的返回值是泛型 V,如果把 call 中计算得到的结果放到这个对象中,就可以利用 call 方法的返回值来获得子线程的执行结果了。

1.3 Callable 和 Runnable 的不同之处

  • 方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run();
  • 返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的;
  • 抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
  • 和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。

2、Future 的主要功能是什么?

2.1 Future 的作用

Future 最主要的作用是,比如当做一定运算的时候(运算过程可能比较耗时,有时会去查数据库,或是繁重的计算,比如压缩、加密等),在这种情况下,如果我们一直在原地等待方法返回,整体程序的运行效率会大大降低。我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。这样一来就可以把整个程序的运行效率提高,是一种异步的思想。

2.2 Callable 和 Future 的关系

首先看一下 Future 接口的代码,一共有 5 个方法,代码如下所示:

public interface Future {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    //第 5 个方法是对第 4 个方法的重载,方法名一样,但参数不一样
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutExceptio
}

Future 相当于一个存储器,存储了 Callable 的 call 方法的任务结果。
Future 的 get 方法来获取 Callable 接口任务执行的结果;
Future 的 isDone 方法来判断任务是否已经执行完毕了;
Future 的 cancel 方法取消任务的执行;
Future 的 isCancelled() 方法判断任务是否被取消。

2.3 用 FutureTask 来创建 Future

除了用线程池的 submit 方法会返回一个 future 对象之外,同样还可以用 FutureTask 来获取 Future 类和任务的结果。

FutureTask 首先是一个任务(Task),然后具有 Future 接口的语义,因为它可以在将来(Future)得到执行的结果。

来看一下 FutureTask 的代码实现:

public class FutureTask implements RunnableFuture{
 ...
}

public interface RunnableFuture extends Runnable, Future {
    void run();
}

可以看到,FutureTask 实现了一个接口,这个接口叫作 RunnableFuture,而 RunnableFuture 是 extends Runnable 和 Future 这两个接口的,它们的关系如下图所示:

既然 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 又实现了 RunnableFuture 接口,所以 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。

3、使用 Future 有哪些注意点?Future 产生新的线程了吗?

3.1 Future 的注意点

  • 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制

即调用 Future 的带超时参数的 get(long timeout, TimeUnit unit)方法。这个方法的作用是,如果在限定的时间内没能返回结果的话,那么便会抛出一个 TimeoutException 异常,随后就可以把这个异常捕获住,或者是再往上抛出去,这样就不会一直卡着了。

  • Future 的生命周期不能后退

Future 的生命周期不能后退,一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来,也不能让一个已经完成计算的 Future 再次重新执行任务。

3.2 Future 产生新的线程了吗?

其实 Callable 和 Future 本身并不能产生新的线程,它们需要借助其他的比如 Thread 类或者线程池才能执行任务。例如,在把 Callable 提交到线程池后,真正执行 Callable 的其实还是线程池中的线程,而线程池中的线程是由 ThreadFactory 产生的,这里产生的新线程与 Callable、Future 都没有关系,所以 Future 并没有产生新的线程。

4、如何利用 CompletableFuture 实现“旅游平台”问题?

4.1 什么是旅游平台问题?

如果想要搭建一个旅游平台,经常会有这样的需求,那就是用户想同时获取多家航空公司的航班信息。所以应该把所有航空公司的航班、票价等信息都获取到,然后再聚合。由于每个航空公司都有自己的服务器,所以分别去请求它们的服务器就可以了,比如请求国航、海航、东航等,如下图所示:

4.2 获取旅游信息的方式

(1)串行获取
比如想获取价格,要先去访问国航,在这里叫作 website 1,然后再去访问海航 website 2,以此类推。当每一个请求发出去之后,等它响应回来以后,才能去请求下一个网站,这就是串行的方式。

这样做的效率非常低下,比如航空公司比较多,假设每个航空公司都需要 1 秒钟的话,那么用户肯定等不及,所以这种方式是不可取的。

(2)并行获取
可以并行地去获取这些机票信息,然后再把机票信息给聚合起来,这样的话,效率会成倍的提高。

这种并行虽然提高了效率,但也有一个缺点,那就是会“一直等到所有请求都返回”。如果有一个网站特别慢,那么你不应该被那个网站拖累,比如说某个网站打开需要二十秒,那肯定是等不了这么长时间的,所以需要一个功能,那就是有超时的获取。

(3)有超时的并行获取
在这种情况下,就属于有超时的并行获取,同样也在并行的去请求各个网站信息。但是规定了一个时间的超时,比如 3 秒钟,那么到 3 秒钟的时候如果都已经返回了那当然最好,把它们收集起来即可;但是如果还有些网站没能及时返回,就把这些请求给忽略掉,这样一来用户体验就比较好了,它最多只需要等固定的 3 秒钟就能拿到信息,虽然拿到的可能不是最全的,但是总比一直等更好。

4.3 实现“有超时的并行获取”的三种方案

(1)线程池实现

public class ThreadPoolDemo {

    ExecutorService threadPool = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo();
        System.out.println(threadPoolDemo.getPrices());
    }

    private Set getPrices() throws InterruptedException {
        Set prices = Collections.synchronizedSet(new HashSet());
        threadPool.submit(new Task(123, prices));
        threadPool.submit(new Task(456, prices));
        threadPool.submit(new Task(789, prices));
        Thread.sleep(3000);
        return prices;
    }

    private class Task implements Runnable {

        Integer productId;
        Set prices;

        public Task(Integer productId, Set prices) {
            this.productId = productId;
            this.prices = prices;
        }

        @Override
        public void run() {
            int price=0;
            try {
                Thread.sleep((long) (Math.random() * 4000));
                price= (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            prices.add(price);
        }
    }
}

在代码中,新建了一个线程安全的 Set,它是用来存储各个价格信息的,把它命名为 Prices,然后往线程池中去放任务。线程池是在类的最开始时创建的,是一个固定 3 线程的线程池。而这个任务在下方的 Task 类中进行了描述,在这个 Task 中我们看到有 run 方法,在该方法里面,我们用一个随机的时间去模拟各个航空网站的响应时间,然后再去返回一个随机的价格来表示票价,最后把这个票价放到 Set 中。这就是我们 run 方法所做的事情。

再回到 getPrices 函数中,我们新建了三个任务,productId 分别是 123、456、789,这里的 productId 并不重要,因为我们返回的价格是随机的,为了实现超时等待的功能,在这里调用了 Thread 的 sleep 方法来休眠 3 秒钟,这样做的话,它就会在这里等待 3 秒,之后直接返回 prices。

此时,如果前面响应速度快的话,prices 里面最多会有三个值,但是如果每一个响应时间都很慢,那么可能 prices 里面一个值都没有。不论你有多少个,它都会在休眠结束之后,也就是执行完 Thread 的 sleep 之后直接把 prices 返回,并且最终在 main 函数中把这个结果给打印出来。

我们来看一下可能的执行结果,一种可能性就是有 3 个值,即 [3815, 3609, 3819](数字是随机的);有可能是 1 个 [3496]、或 2 个 [1701, 2730],如果每一个响应速度都特别慢,可能一个值都没有。

(2)CountDownLatch实现
在这里会有一个优化的空间,比如说网络特别好时,每个航空公司响应速度都特别快,你根本不需要等三秒,有的航空公司可能几百毫秒就返回了,那么我们也不应该让用户等 3 秒。所以需要进行一下这样的改进,看下面这段代码:

public class CountDownLatchDemo {

    ExecutorService threadPool = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
        System.out.println(countDownLatchDemo.getPrices());
    }

    private Set getPrices() throws InterruptedException {
        Set prices = Collections.synchronizedSet(new HashSet());
        CountDownLatch countDownLatch = new CountDownLatch(3);

        threadPool.submit(new Task(123, prices, countDownLatch));
        threadPool.submit(new Task(456, prices, countDownLatch));
        threadPool.submit(new Task(789, prices, countDownLatch));

        countDownLatch.await(3, TimeUnit.SECONDS);
        return prices;
    }

    private class Task implements Runnable {

        Integer productId;
        Set prices;
        CountDownLatch countDownLatch;

        public Task(Integer productId, Set prices,
                CountDownLatch countDownLatch) {
            this.productId = productId;
            this.prices = prices;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            int price = 0;
            try {
                Thread.sleep((long) (Math.random() * 4000));
                price = (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            prices.add(price);
            countDownLatch.countDown();
        }
    }
}

这段代码使用 CountDownLatch 实现了这个功能,整体思路和之前是一致的,不同点在于我们新增了一个 CountDownLatch,并且把它传入到了 Task 中。在 Task 中,获取完机票信息并且把它添加到 Set 之后,会调用 countDown 方法,相当于把计数减 1。

这样一来,在执行 countDownLatch.await(3,
TimeUnit.SECONDS) 这个函数进行等待时,如果三个任务都非常快速地执行完毕了,那么三个线程都已经执行了 countDown 方法,那么这个 await 方法就会立刻返回,不需要傻等到 3 秒钟。

如果有一个请求特别慢,相当于有一个线程没有执行 countDown 方法,来不及在 3 秒钟之内执行完毕,那么这个带超时参数的 await 方法也会在 3 秒钟到了以后,及时地放弃这一次等待,于是就把 prices 给返回了。所以这样一来,我们就利用 CountDownLatch 实现了这个需求,也就是说我们最多等 3 秒钟,但如果在 3 秒之内全都返回了,我们也可以快速地去返回,不会傻等,提高了效率。

(3)CompletableFuture实现

public class CompletableFutureDemo {

    private final CompletableFuture completableFuture = new CompletableFuture();

    public static void main(String[] args)
            throws Exception {
        CompletableFutureDemo completableFutureDemo = new CompletableFutureDemo();
        System.out.println(completableFutureDemo.getPrices());
    }

    private Set getPrices() {
        Set prices = Collections.synchronizedSet(new HashSet());
        CompletableFuture task1 = CompletableFuture.runAsync(new Task(123, prices));
        CompletableFuture task2 = CompletableFuture.runAsync(new Task(456, prices));
        CompletableFuture task3 = CompletableFuture.runAsync(new Task(789, prices));

        CompletableFuture allTasks = CompletableFuture.allOf(task1, task2, task3);
        try {
            allTasks.get(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        } catch (TimeoutException e) {
        }
        return prices;
    }

    private class Task implements Runnable {

        Integer productId;
        Set prices;

        public Task(Integer productId, Set prices) {
            this.productId = productId;
            this.prices = prices;
        }

        @Override
        public void run() {
            int price = 0;
            try {
                Thread.sleep((long) (Math.random() * 4000));
                price = (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            prices.add(price);
        }
    }
}

这里不再使用线程池了,看 getPrices 方法,在这个方法中,用了 CompletableFuture 的 runAsync 方法,这个方法会异步的去执行任务。

有三个任务,并且在执行这个代码之后会分别返回一个 CompletableFuture 对象,把它们命名为 task 1、task 2、task 3,然后执行 CompletableFuture 的 allOf 方法,并且把 task 1、task 2、task 3 传入。这个方法的作用是把多个 task 汇总,然后可以根据需要去获取到传入参数的这些 task 的返回结果,或者等待它们都执行完毕等。我们就把这个返回值叫作 allTasks,并且在下面调用它的带超时时间的 get 方法,同时传入 3 秒钟的超时参数。

这样一来它的效果就是,如果在 3 秒钟之内这 3 个任务都可以顺利返回,也就是这个任务包括的那三个任务,每一个都执行完毕的话,则这个 get 方法就可以及时正常返回,并且往下执行,相当于执行到 return prices。在下面的这个 Task 的 run 方法中,该方法如果执行完毕的话,对于 CompletableFuture 而言就意味着这个任务结束,它是以这个作为标记来判断任务是不是执行完毕的。但是如果有某一个任务没能来得及在 3 秒钟之内返回,那么这个带超时参数的 get 方法便会抛出 TimeoutException 异常,同样会被我们给 catch 住。这样一来它就实现了这样的效果:会尝试等待所有的任务完成,但是最多只会等 3 秒钟,在此之间,如及时完成则及时返回。那么所以利用 CompletableFuture,同样也可以解决旅游平台的问题。它的运行结果也和之前是一样的,有多种可能性。

你可能感兴趣的:(四十六、Callable 和 Future)