JUC基础(二)—— Future接口 及其实现

简介: JUC基础(二)—— Future接口 及其实现

前言

前面我们讲了JUC里面线程池的相关内容,我们在代码中把任务交给线程池,实际上就是一种线程异步的操作,这能提升性能,减少接口相应时间。但是大部分情况下,我们还会在适当的时间后,看看任务有没有完成,又或者从异步线程里取回结果来进行后续操作,那么就用到了本期的内容了


一、Future的原始意义

要想搞清楚异步任务,我们必须先学一点基础,比如Future,回想我们第一次认识Future,大部分都是在使用线程池时,向线程池提交任务,看到返回值为Future类型

d9ffe4f777df47e78952bcaf52886396.png

我们点开Future,看看它的方法

1676bc7ef0ab424d89036dac41f9dac6.png

不难发现,Future就是一张凭证,我们交给线程池一个任务,线程池返回给我们一个凭证。 我们可以通过这个凭证去


  • 咨询任务的执行情况
  • 取消任务
  • 取出任务结果

e4d5b1fc82e7477589b65b29aa89822e.png

一个贴近现实的例子就是,我们去寄快递,店家会给返回给我们一个快递单号,我们后续可以通过这快递单号咨询快递到哪了,或者取走快递也需要使用这快递单号,这个给我们的快递单号就是Future


二、FutureTask 和 CompletableFuture

前面我们说了,Future的原始意义就是一个异步任务的“凭证”,可以通过凭证查询。但Future是一个接口,JUC里Future的实现类经常兼并着其他作用,所以我们对Future的实现类进行讨论时,往往会发现,这些实现类的功能已经超出了上述的“凭证”的范畴,我们现在要讨论的就是它的两个增强实现类 FutureTask 和 CompletableFuture


1.FutureTask

Future最常用的实现类就是FutureTask,顾名思义,FutureTask 是由Future 和 Task 组成,task即一个可执行的任务。我们可以看一下类图

d9568f451f184a119690fe7729423011.png

FutureTask 同时实现了Future 和 Runnable。说白了,这个类就是个粘合剂,既可以被执行,又能存储任务的执行结果,同时还可以取消任务、查看任务的状态、获取执行结果。


即FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取执行的返回结果,等于说身兼两职,以一个对象,解决了future只能观测异步结果,runnable只能执行的问题


我们可以写个例子:


    public static void main(String[] args) throws Exception {
        // 创建线程池
        ThreadPoolExecutor tp = new ThreadPoolExecutor(3, 5, 2000L,
             TimeUnit.SECONDS, new ArrayBlockingQueue<>(200),
             new ThreadFactory() {
                     private AtomicInteger threadNumber = new AtomicInteger(1);
                     @Override
                     public Thread newThread(Runnable r) {
                         return new Thread(r, "money caculate thread: " + threadNumber.getAndAdd(1));
                     }
             },
             new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 创建futureTask
        FutureTask<String> futureTask = new FutureTask(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(3000);
                return "返回任务结果";
            }
        });
        // 提交任务
        Future<?> submit = tp.submit(futureTask);
        String s = futureTask.get();
        System.out.println("通过futureTask拿结果: " + s);
        String o = (String)submit.get();
        System.out.println("通过线程池凭证拿结果: " + o);
    }

结果如下:

896ab4a03a8946a68fafdd3021ab8878.png



当然,如果我们不手动建FutureTask,而是直接向线程池提交一个任务。其内部实际上也是会为我们自动建一个FutureTask,然后返回给我们的,这也说明了FutureTask其实是相当基础的一个类。


Future future = tp.submit(new Runnable()…)

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

2.CompletableFuture

学完了FutureTask,我们不难得出,对于像提交任务,检查任务状态,获取任务结果这些场景,FutureTask已经做到了。但这并不意味着,他没有问题,我们来看一个实际场景:


某一个接口耗时太长,需要优化。经过分析,发现该接口内有大量长耗时的任务,虽然我们必须等待这些任务都完成才算结束,但这些接口本身没有关联。因此,我们考虑使用线程池,并发的执行这些任务。

但大量的FutureTask不知道谁先执行完,主线程最终还是要线性的问询这些任务的执行结果,效率未最大化

且手动一个个的进行future.isDone 或 future.get判断,导致代码十分臃肿。


上述场景就暴露了FutureTask的不足,其实不仅于此,Future 的问题在于


  • 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的(尽管可以设置阻塞时间)。所以,除了等待你别无他法;
  • 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
  • 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
  • 没有异常处理:Future接口中没有关于异常处理的方法;

为了解决上述问题,Future接口的另一个“更强功能”的实现类CompletableFuture也就应运而生,照例,我们先看类图

e80f3e0d8853405ab0f6317128f58998.png


可以看到与FutureTask相比,CompletableFuture没有实现Runnable,但实现了CompletionStage,这个CompletionStage是JDK8引入的内容,可以对异步执行的任务分阶段,并建立任务间的依赖关系,我们看下它定义的方法:

d776a5ed2ee14d079477c9a7814088f5.png


别被这么长的方法吓到,其实这些方法都是用来建立任务执行顺序,建立任务依赖关系的。本次我们主要说的还是偏使用层面,观察这些方法的名字以及出入参,我们不难看出规律,以方法名为例,名字中带有以下关键字的


  • apply:上阶段结果作下阶段参数继续执行,最后有返回结果
  • accept:上阶段结果作下阶段参数继续执行,最后无返回结果
  • run:上阶段执行完,下阶段执行,彼此不存在参数上的依赖

当然,我们也可以从另一个角度看,方法名带有下列关键字的:


  • both:前两阶段同时执行完毕,然后执行下一阶段
  • either:前两阶段任一执行完毕,执行下一阶段

出于篇幅考虑,我们在这里并不会一一解释这些方法,关于CompletableFuture的详细分析,可以参考另一篇博客:CompletableFuture使用详解


我们现在主要解决我们在前面提出的那个现实场景:


我们希望大量无关联任务并发执行,当所有任务完成的时候,我们能最快知晓,从而减少接口耗时。


这里我们就可以利用到CompletableFuture 里面的supplyAsync异步提交任务,然后使用 allOf 方法组合所有任务


f415d86461cf47fcad39d7d1b1b74f62.png


具体的代码样式如下:

 // 分别向线程池提交任务
 CompletableFuture<T> future1 = CompletableFuture.supplyAsync(() -> a.method(), tp);
 CompletableFuture<T> future2 = CompletableFuture.supplyAsync(() -> b.method(), tp);
 CompletableFuture<T> future3 = CompletableFuture.supplyAsync(() -> c.method(), tp);
 CompletableFuture<Void> mixedFuture = CompletableFuture.allOf(future1, future2, future3);
 // 所有任务都完成
 if (mixedFuture.isDone()) {
   // some finish job    
 }

如果你够细心,应该能发现两个supplyAsync方法,一个需要传线程池,另一个不需要。不需要是因为CompletableFuture内会创建一个ForkJoinPool.commonPool()线程池,ForkJoinPool线程池是一个独特的线程池,我们在JUC基础(一)—— 线程池里提到过,但没有仔细分析其功能和实现,这块内容也会在后续推出详解。


目录
相关文章
|
2月前
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
高并发编程之多线程锁和Callable&Future 接口
47 1
|
26天前
|
存储 并行计算 Java
Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)
Java8中JUC包同步工具类深度解析(Semaphore,CountDownLatch,CyclicBarrier,Phaser)
19 2
|
2月前
|
Java
Java并发Futures和Callables类
Java程序`TestThread`演示了如何在多线程环境中使用`Futures`和`Callables`。它创建了一个单线程`ExecutorService`,然后提交两个`FactorialService`任务,分别计算10和20的阶乘。每个任务返回一个`Future`对象,通过`get`方法获取结果,该方法会阻塞直到计算完成。计算过程中模拟延迟以展示异步执行。最终,打印出10!和20!的结果。
28 10
|
2月前
|
存储 安全 Java
Java并发基础:BlockingQueue和BlockingDeque接口的区别?
BlockingQueue 和 BlockingDeque 它们都支持在并发编程中的线程安全操作,但是,这两个接口之间存在一些关键的区别,主要在于它们所支持的操作和数据结构的特性,
Java并发基础:BlockingQueue和BlockingDeque接口的区别?
|
2月前
|
Java
Java并发编程:理解并使用Future和Callable接口
【2月更文挑战第25天】 在Java中,多线程编程是一个重要的概念,它允许我们同时执行多个任务。然而,有时候我们需要等待一个或多个线程完成,然后才能继续执行其他任务。这就需要使用到Future和Callable接口。本文将深入探讨这两个接口的用法,以及它们如何帮助我们更好地管理多线程。
|
8月前
|
存储 Java
并发编程系列教程(09) - Callable与Future模式
并发编程系列教程(09) - Callable与Future模式
32 0
|
9月前
|
Java
异步编程 - 04 基于JDK中的Future实现异步编程(上)_Future & FutureTask 源码解析
异步编程 - 04 基于JDK中的Future实现异步编程(上)_Future & FutureTask 源码解析
58 0
|
设计模式 Java API
【JUC基础】15. Future模式
Future 模式是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。当我们需要调用一个函数方法时,如果这个函数执行得很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。因此,我们可以让被调者立即返回,让它在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获得需要的数据。
118 0
【JUC基础】15. Future模式
|
Java
Future 任务机制和 FutureTask 的实现原理及使用方法
Future 任务机制和 FutureTask 的实现原理及使用方法
146 0
|
容器
并发编程-21J.U.C组件拓展之Future和FutureTask
并发编程-21J.U.C组件拓展之Future和FutureTask
53 0