Runnable接口存在的缺陷

    1)不能返回一个返回值
    第一个缺陷,对于 Runnable 而言,它不能返回一个返回值,虽然可以利用其他的一些办法,比如在 Runnable 方法中写入日志文件或者修改某个共享的对象的办法,来达到保存线程执行结果的目的,但这种解决问题的行为千曲百折,属于曲线救国,效率着实不高。
    实际上,在很多情况下执行一个子线程时,我们都希望能得到执行的任务的结果,也就是说,我们是需要得到返回值的,比如请求网络、查询数据库等。可是 Runnable 不能返回一个返回值,这是它第一个非常严重的缺陷。

    2)不能够抛出Checked Exception
    我们在任务内容定义阶段,不能按照我们的想法抛出异常,显式的使用throw new Exception();,需要使用try{}catch{}语句捕获异常,不能够向上层抛出。

    1. public interface Runnable {
    2. public abstract void run();
    3. }

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

    Callable 接口
    Callable 是一个类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 接口的类都是可以被其他线程执行的任务。 我们看一下 Callable 的源码:

    1. public interface Callable<V> {
    2. V call() throws Exception;
    3. }

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

    Callable 和 Runnable 的不同之处:

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

    Future 类
    Future 最主要的作用是,比如当做一定运算的时候,运算过程可能比较耗时,有时会去查数据库,或是繁重的计算,比如压缩、加密等,在这种情况下,如果我们一直在原地等待方法返回,显然是不明智的,整体程序的运行效率会大大降低。我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。这样一来就可以把整个程序的运行效率提高,是一种异步的思想。
    Callable 接口相比于 Runnable 的一大优势是可以有返回结果,我们可以通过用Future 类的 get 方法来获取 。因此,Future 相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过 Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之 Future 的功能比较丰富。

    Future 的方法和用法
    Future 接口的代码,一共有 5 个方法,代码如下所示:

    1. public interface Future<V> {
    2. boolean cancel(boolean mayInterruptIfRunning);
    3. boolean isCancelled();
    4. boolean isDone();
    5. V get() throws InterruptedException, ExecutionException;
    6. V get(long timeout, TimeUnit unit)
    7. throws InterruptedException, ExecutionException, TimeoutExceptio
    8. }

    get() 方法:获取结果
    get 方法最主要的作用就是获取任务执行的结果,该方法在执行时的行为取决于 Callable 任务的状态,可能会发生以下 5 种情况。

    • 最常见的就是当执行 get 的时候,任务已经执行完毕了,可以立刻返回,获取到任务执行的结果。
    • 任务还没有结果,这是有可能的,比如我们往线程池中放一个任务,线程池中可能积压了很多任务,还没轮到我去执行的时候,就去 get 了,在这种情况下,相当于任务还没开始;还有一种情况是任务正在执行中,但是执行过程比较长,所以我去 get 的时候,它依然在执行的过程中。无论是任务还没开始或在进行中,我们去调用 get 的时候,都会把当前的线程阻塞,直到任务完成再把结果返回回来。
    • 任务执行过程中抛出异常,一旦这样,我们再去调用 get 的时候,就会抛出 ExecutionException 异常,不管我们执行 call 方法时里面抛出的异常类型是什么,在执行 get 方法时所获得的异常都是 ExecutionException。
    • 任务被取消了,如果任务被取消,我们用 get 方法去获取结果时则会抛出 CancellationException。
    • 任务超时,get 方法有一个重载方法,那就是带延迟参数的,调用了这个带延迟参数的 get 方法之后,如果 call 方法在规定时间内正常顺利完成了任务,那么 get 会正常返回;但是如果到达了指定时间依然没有完成任务,get 方法则会抛出 TimeoutException,代表超时了。

    image.png

    isDone() 方法:判断是否执行完毕
    这个方法如果返回 true 则代表执行完成了;如果返回 false 则代表还没完成。但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。
    我们用一个代码示例来看一看,代码如下所示:

    1. public class GetException {
    2. public static void main(String[] args) {
    3. ExecutorService service = Executors.newFixedThreadPool(20);
    4. Future<Integer> future = service.submit(new CallableTask());
    5. try {
    6. for (int i = 0; i < 5; i++) {
    7. System.out.println(i);
    8. Thread.sleep(500);
    9. }
    10. System.out.println(future.isDone());
    11. future.get();
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. } catch (ExecutionException e) {
    15. e.printStackTrace();
    16. }
    17. }
    18. static class CallableTask implements Callable<Integer> {
    19. @Override
    20. public Integer call() throws Exception {
    21. throw new IllegalArgumentException("Callable抛出异常");
    22. }
    23. }
    24. }

    代码的执行结果是这样的:

    0
    1
    2
    3
    4
    true
    java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Callable抛出异常
    ...
    

    这段代码证明了三件事情:第一件事情,即便任务抛出异常,isDone 方法依然会返回 true;第二件事情,虽然抛出的异常是 IllegalArgumentException,但是对于 get 而言,它抛出的异常依然是 ExecutionException;第三个事情,虽然在任务执行一开始时就抛出了异常,但是真正要等到我们执行 get 的时候,才看到了异常。
    **
    cancel 方法:取消任务的执行
    如果不想执行某个任务了,则可以使用 cancel 方法,会有以下三种情况:
    第一种情况最简单,那就是当任务还没有开始执行时,一旦调用 cancel,这个任务就会被正常取消,未来也不会被执行,那么 cancel 方法返回 true。
    第二种情况也比较简单。如果任务已经完成,或者之前已经被取消过了,那么执行 cancel 方法则代表取消失败,返回 false。因为任务无论是已完成还是已经被取消过了,都不能再被取消了。
    第三种情况比较特殊,就是这个任务正在执行,这个时候执行 cancel 方法是不会直接取消这个任务的,而是会根据我们传入的参数做判断。cancel 方法是必须传入一个参数,该参数叫作 mayInterruptIfRunning,它是什么含义呢?如果传入的参数是 true,执行任务的线程就会收到一个中断的信号,正在执行的任务可能会有一些处理中断的逻辑,进而停止,这个比较好理解。如果传入的是 false 则就代表不中断正在运行的任务,也就是说,本次 cancel 不会有任何效果,同时 cancel 方法会返回 false。
    那么如何选择传入 true 还是 false 呢?
    传入 true 适用的情况是,明确知道这个任务能够处理中断。
    传入 false 适用于什么情况呢?

    • 如果我们明确知道这个线程不能处理中断,那应该传入 false。
    • 我们不知道这个任务是否支持取消(是否能响应中断),因为在大多数情况下代码是多人协作的,对于这个任务是否支持中断,我们不一定有十足的把握,那么在这种情况下也应该传入 false。
    • 如果这个任务一旦开始运行,我们就希望它完全的执行完毕。在这种情况下,也应该传入 false。

    isCancelled() 方法:判断是否被取消
    最后一个方法是 isCancelled 方法,判断是否被取消,它和 cancel 方法配合使用,比较简单。

    用 FutureTask 来创建 Future
    除了用线程池的 submit 方法会返回一个 future 对象之外,同样还可以用 FutureTask 来获取 Future 类和任务的结果。
    FutureTask 首先是一个任务(Task),然后具有 Future 接口的语义,因为它可以在将来(Future)得到执行的结果。
    我们来看一下 FutureTask 的代码实现:

    public class FutureTask<V> implements RunnableFuture<V>{
     ...
    }
    

    它实现了一个接口,这个接口叫作 RunnableFuture。我们再来看一下 RunnableFuture 接口的代码实现:

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

    可以看出,它是 extends Runnable 和 Future 这两个接口的,它们的关系如下图所示:
    Java [Future] - 图2
    既然 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 又实现了 RunnableFuture 接口,所以 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
    典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。
    代码来演示一下:

    /**
     * 描述:     演示 FutureTask 的用法
     */
    public class FutureTaskDemo {
    
        public static void main(String[] args) {
            Task task = new Task();
            FutureTask<Integer> integerFutureTask = new FutureTask<>(task);
            new Thread(integerFutureTask).start();
            try {
                System.out.println("task运行结果:"+integerFutureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    class Task implements Callable<Integer> {
    
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程正在计算");
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                sum += i;
            }
            return sum;
        }
    
    }
    

    Future 的注意点

    • 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制。对于 Future 而言,第一个注意点就是,当 for 循环批量获取 Future 的结果时容易 block,在调用 get 方法时,应该使用 timeout 来限制。
    • Future 的生命周期不能后退,一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来,也不能让一个已经完成计算的 Future 再次重新执行任务。

    旅游平台问题(同时调用多个依赖的外部接口)
    什么是旅游平台问题呢?如果想要搭建一个旅游平台,经常会有这样的需求,那就是用户想同时获取多家航空公司的航班信息。比如,从北京到上海的机票钱是多少?有很多家航空公司都有这样的航班信息,所以应该把所有航空公司的航班、票价等信息都获取到,然后再聚合。由于每个航空公司都有自己的服务器,所以分别去请求它们的服务器就可以了,比如请求国航、海航、东航等,如下图所示:
    image.png

    串行获取
    image.png
    任务消耗时间为三次请求的耗时总和。
    并发获取
    image.png
    任务耗时为最长的那次任务耗时。
    有超时时间的并行获取
    image.png
    如果任务都在超时时间内返回,则全部返回;如果有任务超过超时间还没有返回就丢弃。

    CompletableFuture
    我们再来看一下用 CompletableFuture 来实现这个功能的用法,代码如下所示:

    public class CompletableFutureDemo {
    
        public static void main(String[] args)
                throws Exception {
            CompletableFutureDemo completableFutureDemo = new CompletableFutureDemo();
            System.out.println(completableFutureDemo.getPrices());
        }
    
        private Set<Integer> getPrices() {
            Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
            CompletableFuture<Void> task1 = CompletableFuture.runAsync(new Task(123, prices));
            CompletableFuture<Void> task2 = CompletableFuture.runAsync(new Task(456, prices));
            CompletableFuture<Void> task3 = CompletableFuture.runAsync(new Task(789, prices));
            CompletableFuture<Void> 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<Integer> prices;
    
            public Task(Integer productId, Set<Integer> 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);
            }
        }
    
    }
    

    **