跳到正文
hello world

29. runAsync 和 supplyAsync 的区别

发布于阅读量 0

29. runAsync 和 supplyAsync 的区别

上一节开始接触 CompletableFuture

这一节先看两个最基础的入口方法:

runAsync
supplyAsync

它们都能异步执行任务。

区别也很直接:

runAsync:异步执行一个没有返回值的任务;
supplyAsync:异步执行一个有返回值的任务。

这个区别看起来简单,但后面选方法时经常会用到。

比如 PDF 水印任务,如果我只是想让它在后台执行,不关心返回结果,可以用 runAsync

如果我还想拿到输出文件路径、处理结果、失败原因,那就应该用 supplyAsync


先看 runAsync

runAsync 适合执行没有返回值的任务。

比如:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("开始处理 PDF");
    sleep(3000);
    System.out.println("PDF 处理完成");
});

注意它的返回类型是:

CompletableFuture<Void>

这里的 Void 表示这个异步任务没有结果。

也就是说,任务执行完就完了,不会返回一个具体的数据。


runAsync 示例

新建类:

com.succos.completablefuture.RunAsyncDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class RunAsyncDemo {

    public static void main(String[] args) {

        System.out.println("main 开始:" + Thread.currentThread().getName());

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("runAsync 开始执行:"
                    + Thread.currentThread().getName());

            sleep(3000);

            System.out.println("runAsync 执行完成:"
                    + Thread.currentThread().getName());
        });

        System.out.println("main 继续往下执行:" + Thread.currentThread().getName());

        future.join();

        System.out.println("main 结束:" + Thread.currentThread().getName());
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

这段代码里,异步任务只是打印日志,没有返回值。

所以用 runAsync 比较合适。

最后用了:

future.join();

main 线程等待异步任务执行完。


join 在这里做什么

这里先简单理解:

future.join();

它的作用是等待 CompletableFuture 完成。

如果不写这一行,main 线程可能很快执行结束。

学习阶段,为了看清楚异步任务的完整日志,我会先写上 join()

后面会单独整理 join()get() 的区别。

这一节先知道:

runAsync 提交任务后,main 不会自动等;
想等任务完成,可以调用 join。

再看 supplyAsync

supplyAsync 适合执行有返回值的任务。

比如:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("开始处理 PDF");
    sleep(3000);
    return "output/test-watermark.pdf";
});

这里返回的是字符串:

"output/test-watermark.pdf"

所以整个异步结果类型是:

CompletableFuture<String>

这个 String 就是后续流程可以继续使用的结果。


supplyAsync 示例

新建类:

com.succos.completablefuture.SupplyAsyncDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class SupplyAsyncDemo {

    public static void main(String[] args) {

        System.out.println("main 开始:" + Thread.currentThread().getName());

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync 开始执行:"
                    + Thread.currentThread().getName());

            sleep(3000);

            System.out.println("supplyAsync 执行完成:"
                    + Thread.currentThread().getName());

            return "output/test-watermark.pdf";
        });

        System.out.println("main 继续往下执行:" + Thread.currentThread().getName());

        String result = future.join();

        System.out.println("main 拿到结果:" + result);
        System.out.println("main 结束:" + Thread.currentThread().getName());
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

这里的重点是:

String result = future.join();

因为 future 是:

CompletableFuture<String>

所以 join() 能拿到一个 String 结果。

这和 runAsyncCompletableFuture<Void> 不一样。


runAsync 和 supplyAsync 的核心区别

可以先用一张表记住:

方法 任务类型 返回类型 适合场景
runAsync 没有返回值的任务 CompletableFuture<Void> 执行完就结束,不需要结果
supplyAsync 有返回值的任务 CompletableFuture<T> 执行完还要拿结果

如果任务逻辑只是:

System.out.println("处理完成");

这种没有结果的任务,用 runAsync

如果任务逻辑是:

return "output/test-watermark.pdf";

这种需要返回结果的任务,用 supplyAsync


PDF 水印里怎么选

回到 PDF 水印项目。

如果我只是想后台处理 PDF,不关心返回值,可以这样:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    addWatermark(file);
});

这个写法适合:

只想让任务异步执行;
任务内部自己打印日志;
外部不需要拿输出路径;
外部不需要统计结果。

但实际业务里,我一般不会只满足于这样。

PDF 处理完成后,我大概率还要知道:

输出文件路径;
处理是否成功;
失败原因;
耗时;
后续下载地址。

这时候更适合:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return addWatermark(file);
});

这里假设 addWatermark(file) 返回处理后的文件路径。

这样后面就可以继续:

future.thenApply(path -> {
    return "http://localhost:8080/download?file=" + path;
});

所以 PDF 水印这种场景,后面更多会用 supplyAsync


runAsync 的 PDF 示例

先看一个 runAsync 版本。

新建类:

com.succos.completablefuture.RunAsyncPdfDemo

代码如下:

package com.succos.completablefuture;

import java.io.File;
import java.util.concurrent.CompletableFuture;

public class RunAsyncPdfDemo {

    public static void main(String[] args) {

        File file = new File("input/test.pdf");

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理:"
                    + file.getName());

            sleep(3000);

            System.out.println(Thread.currentThread().getName()
                    + " 处理完成:"
                    + file.getName());
        });

        future.join();

        System.out.println("main 结束");
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

这个版本只负责执行。

任务结束后,外面拿不到输出路径。

它适合演示异步执行,但不太适合后面的结果汇总。


supplyAsync 的 PDF 示例

再看一个 supplyAsync 版本。

新建类:

com.succos.completablefuture.SupplyAsyncPdfDemo

代码如下:

package com.succos.completablefuture;

import java.io.File;
import java.util.concurrent.CompletableFuture;

public class SupplyAsyncPdfDemo {

    public static void main(String[] args) {

        File file = new File("input/test.pdf");

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理:"
                    + file.getName());

            sleep(3000);

            String targetPath = "output/"
                    + file.getName().replace(".pdf", "-watermark.pdf");

            System.out.println(Thread.currentThread().getName()
                    + " 处理完成:"
                    + file.getName());

            return targetPath;
        });

        String targetPath = future.join();

        System.out.println("输出路径:" + targetPath);
        System.out.println("main 结束");
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

这个版本执行完以后,可以拿到:

output/test-watermark.pdf

这就比 runAsync 更适合业务处理。

因为输出路径可以继续用于:

生成下载地址;
保存数据库;
返回给前端;
统计任务结果。

supplyAsync 后面可以继续 thenApply

supplyAsync 的优势不只是能 join() 拿结果。

它更适合接后续流程。

比如:

CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> {
            sleep(3000);
            return "output/test-watermark.pdf";
        })
        .thenApply(path -> {
            return "http://localhost:8080/download?file=" + path;
        });

这里第一步返回的是文件路径:

output/test-watermark.pdf

第二步把路径转换成下载地址:

http://localhost:8080/download?file=output/test-watermark.pdf

所以最后的 future 类型仍然是:

CompletableFuture<String>

只不过这个 String 已经从“文件路径”变成了“下载地址”。

这种链式处理,就是 CompletableFutureFuture 顺手的地方。


runAsync 后面也能接 thenRun

runAsync 没有结果,所以后面不能拿上一步的返回值。

但可以接:

CompletableFuture<Void> future = CompletableFuture
        .runAsync(() -> {
            System.out.println("处理 PDF");
        })
        .thenRun(() -> {
            System.out.println("PDF 处理结束后,执行下一步");
        });

thenRun() 不接收上一步结果。

因为上一步本来也没有结果。

它只是表示:

上一步执行完以后,再执行这一段。

所以如果后续步骤不需要上一步的返回值,可以用 thenRun()

但如果后续步骤需要路径、结果对象,就应该用 supplyAsync + thenApplythenAccept


默认线程池的问题

无论是:

CompletableFuture.runAsync(() -> {
    // 任务
});

还是:

CompletableFuture.supplyAsync(() -> {
    return "结果";
});

如果不传线程池,默认都会使用公共线程池。

线程名可能是:

ForkJoinPool.commonPool-worker-1

学习时可以这么写。

但 PDF 处理这种任务,我不建议一直用默认公共线程池。

因为 PDF 水印可能比较重,会占用 CPU、内存、磁盘 IO。

更稳的写法是传入自定义线程池:

CompletableFuture.supplyAsync(() -> {
    return addWatermark(file);
}, pdfExecutor);

这个后面会单独整理。


使用自定义线程池的写法

先简单看一下写法。

ThreadPoolExecutor pdfExecutor = new ThreadPoolExecutor(
        3,
        3,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "output/test-watermark.pdf";
}, pdfExecutor);

第二个参数:

pdfExecutor

就是指定这个异步任务用哪个线程池执行。

这样 PDF 处理任务就不会跑到默认公共线程池里。

后面写批量 PDF 处理时,这个非常重要。


两个方法怎么记

我现在这样记:

runAsync 里的 run,像 Runnable,没有返回值;
supplyAsync 里的 supply,像提供一个结果,有返回值。

也可以更直白一点:

不需要结果:runAsync;
需要结果:supplyAsync。

PDF 水印任务大多数情况下需要结果。

所以后面我会更多使用 supplyAsync


这一节小结

这一节我主要记住几点:

1. runAsync 用来异步执行没有返回值的任务;
2. runAsync 返回 CompletableFuture<Void>;
3. supplyAsync 用来异步执行有返回值的任务;
4. supplyAsync 返回 CompletableFuture<T>;
5. PDF 处理如果只执行不关心结果,可以用 runAsync;
6. PDF 处理如果要拿输出路径、结果对象,更适合 supplyAsync;
7. 不传线程池时,会使用默认公共线程池;
8. 真实 PDF 任务最好传入自定义线程池。

用一句话总结:

runAsync 负责“异步干一件事”,supplyAsync 负责“异步干完一件事并给我一个结果”。

下一节继续看 join

因为无论是 runAsync 还是 supplyAsync,任务提交后都不会自动等待。要在合适的位置等待任务完成,就会用到 join()