>

CompletableFuture异步任务的简单使用

一、FutureTask

1、Runnable接口

  提到Callable接口,就一定要提到实现线程的三种方式。第一种是继承Thread类,一种是实现Runnable接口,最后一种就是我们这里说到的实现Callable接口。前两者是比较常见的实现多线程的方式,但是它们都有一个致命的问题,那就是没法获取返回值。

 

  使用继承Thread类的方式实现多线程,其本质和实现Runnable接口相同,通过观察Thread的构造方法不难发现,它传入的参数target就是实现了Runnable接口

public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
    init(group, target, name, stackSize);
}

然后调用Runnable接口的run方法,最终达到和实现Runnable接口相同的目的。

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

2、Callable接口

于是在Java 1.5就提供了Callable接口来实现这一场景,而Future和Future Task就可以和Callable接口配合起来使用。

Runnable

复制代码
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
复制代码

Callable

复制代码
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
复制代码

观察对应的接口,我们可以很直观的得到两个点:

不能返回一个返回值
不能抛出Exception
  想要使用Callable接口实现多线程,就需要和Future类配合,通过Future可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是Runnable做不到的,Callable的功能要比Runnable强大。

3、Future接口

  Future就是对于具体的Runnable或者Callable任务(因为Future接口的实现类FutureTask既可以接受Runnable接口的参数进行实例化,也可以接收Callable接口的参数进行实例化)的执行结果进行取消、查询是否完成、获取结果。

  必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

4、代码测试案例

业务类

复制代码
public class Task implements Callable<String> {
    private String taskName;

    public Task() {
    }

    public Task(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public String call() throws Exception {
        Random ra = new Random();
        // 业务执行时间
        int time = ra.nextInt(10);
        System.out.println(this.taskName + "需要执行:" + time + " s");
        TimeUnit.SECONDS.sleep(time);
        return time + "s的" + this.taskName + "执行返回";
    }
}
复制代码

测试案例一:使用线程池执行五个多线程任务,不获取返回结果

代码实现思路:

1、使用Callable实现多线程,所以一定要有一个实现了Callable的任务类

2、由于Future只是一个接口,所以在实现多线程的时候需要借助其实现类FutureTask

3、将实现Callable接口的任务封装成FutureTask类的对象

4、交由线程池执行(线程池能够接收实现Runnable接口和Callable接口的对象)

5、记得关闭线程池资源(公共线程池除外)

复制代码
public class FutureTaskTest01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        
        long startTime = System.currentTimeMillis();
        FutureTask<String> task1 = new FutureTask<String>(new Task("任务1"));
        FutureTask<String> task2 = new FutureTask<String>(new Task("任务2"));
        FutureTask<String> task3 = new FutureTask<String>(new Task("任务3"));
        FutureTask<String> task4 = new FutureTask<String>(new Task("任务4"));
        FutureTask<String> task5 = new FutureTask<String>(new Task("任务5"));

        ExecutorService executor = Executors.newFixedThreadPool(5);
        executor.submit(task1);
        executor.submit(task2);
        executor.submit(task3);
        executor.submit(task4);
        executor.submit(task5);

        executor.shutdownNow();
        long endTime = System.currentTimeMillis();

        System.out.println("任务执行总时间:" + (endTime - startTime)+" ms");
    }
}
复制代码

返回结果:

不难发现执行任务时是非阻塞式的

测试案例二:获取返回结果(获取实现Callable接口的返回结果时,一定要区别与实现Runnable接口的方式的submit和execute方法,此处只需要调用对应的FutureTask任务的get方法即可,前文有解释)

复制代码
public class FutureTaskTest01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        long startTime = System.currentTimeMillis();


        FutureTask<String> task1 = new FutureTask<String>(new Task("任务1"));
        FutureTask<String> task2 = new FutureTask<String>(new Task("任务2"));
        FutureTask<String> task3 = new FutureTask<String>(new Task("任务3"));
        FutureTask<String> task4 = new FutureTask<String>(new Task("任务4"));
        FutureTask<String> task5 = new FutureTask<String>(new Task("任务5"));

        ExecutorService executor = Executors.newFixedThreadPool(5);
        executor.submit(task1);
        executor.submit(task2);
        executor.submit(task3);
        executor.submit(task4);
        executor.execute(task5);

        System.out.println(task1.get());
        System.out.println(task2.get());
        System.out.println(task3.get());
        System.out.println(task4.get());
        System.out.println(task5.get());
        // 关闭线程池
        executor.shutdownNow();
        long endTime = System.currentTimeMillis();

        System.out.println("任务执行总时间:" + (endTime - startTime)+" ms");
    }
}
复制代码

测试结果:

不难发现,调用get方法获取返回结果是阻塞式的

5、总结

Future 注意事项

  • 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制
  • Future 的生命周期不能后退。一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来

 

Future的局限性

从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:

并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的;
无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如汇总数据,但Future却没有提供这样的能力;
无法组合多个任务:如果你运行了5个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
没有异常处理:Future接口中没有关于异常处理的方法,这就会导致外层无法感知内部的处理情况;

二、CompletionService

  Callable + Future 可以实现多个task并行执行,但是如果遇到前面的task执行较慢时,需要阻塞等待前面的task执行完后面task才能取得结果(即调用get方法获取返回结果是阻塞式的)。而CompletionService的主要功能就是一边生成任务,一边获取任务的返回值。让两件事分开执行,任务之间不会互相阻塞,可以实现先执行完的先取结果,不再依赖任务顺序了。


  内部通过阻塞队列 + FutureTask,实现了任务先完成可优先获取到,即结果按照完成先后顺序排序,内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果

1、代码测试案例

代码的实现方式上与FutureTask有略微的区别

代码实现思路:

1、创建一个线程池

2、将线程池封装为一个CompletionService对象

3、借助CompletionService对象来提交执行对应的多线程任务

4、调用CompletionService对象的take().get()方法获取返回结果

复制代码
public class CompletionServiceTest01 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {

        long startTime = System.currentTimeMillis();
        ExecutorService executor = Executors.newFixedThreadPool(5);
        CompletionService<String> comple = new ExecutorCompletionService<>(executor);
        int num = 5;

        for (int i = 0; i < num; i++) {
            comple.submit(new Task("任务" + i));
        }
        for (int i = 0; i < num; i++) {
            System.out.println(comple.take().get());
        }

        long endTime = System.currentTimeMillis();
        System.out.println("总时间:" + (endTime - startTime));
    }
}
复制代码

测试结果:

2、源码实现原理

不难发现使用CompletionService来执行多线程任务时,调用get方法获取返回结果的时候不再是阻塞式的获取,而是任务执行完毕就直接返回。

前文有说过,这个非阻塞式的得到执行结果是借助队列实现的。当我们看到take方法,第一反应也应该马上想到队列的api。

源码逻辑:

1、当我们调用take方法获取返回结果时,会调用到ExecutorCompletionService类的take方法

2、take方法会去completionQueue队列中获取,而该队列就是一个存放Future的BlockingQueue

3、该队列默认是一个LinkedBlockingQueue

 

4、当然我们也可以自己指定一个对应的队列(比如ArrayBlockingQueue)

5、调用submit方式时,会初始化一个QueueingFuture对象

6、然后将对应的task任务,交给FutureTask类,完成对应任务的执行

7、当FutureTask任务执行完成后,会调用finishCompletion方法,该方法会调用done方法

8、done方法在QueueingFuture类中进行了重写,即完成将task任务添加到completionQueue队列中的目的

9、即调用take方法的时候,获取到的就是处理后的task(FutureTask),然后调用对应的get方法获取结果

应用场景总结

  • 当需要批量提交异步任务的时候建议使用CompletionService。CompletionService将线程池Executor和阻塞队列BlockingQueue的功能融合在了一起,能够让批量异步任务的管理更简单。
  • CompletionService能够让异步任务的执行结果有序化。先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如Forking Cluster这样的需求。
  • 线程池隔离。CompletionService支持自己创建线程池,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。

三、CompletableFuture

  简单的任务,用Future获取结果还好,但我们并行提交的多个异步任务,往往并不是独立的,很多时候业务逻辑处理存在串行[依赖]、并行、聚合的关系。如果要我们手动用 Fueture 实现,是非常麻烦的。

  CompletableFuture是Future接口的扩展和增强。CompletableFuture实现了Future接口,并在此基础上进行了丰富地扩展,完美地弥补了Future上述的种种问题。更为重要的是,CompletableFuture实现了对任务的编排能力。借助这项能力,我们可以轻松地组织不同任务的运行顺序、规则以及方式。从某种程度上说,这项能力是它的核心能力。

详见另一篇博文:CompletableFuture

 

参考文章:

https://blog.csdn.net/qq_44377709/article/details/121717160

 

posted @   字节悦动  阅读(300)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示

目录导航