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 结果。
这和 runAsync 的 CompletableFuture<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 已经从“文件路径”变成了“下载地址”。
这种链式处理,就是 CompletableFuture 比 Future 顺手的地方。
runAsync 后面也能接 thenRun
runAsync 没有结果,所以后面不能拿上一步的返回值。
但可以接:
CompletableFuture<Void> future = CompletableFuture
.runAsync(() -> {
System.out.println("处理 PDF");
})
.thenRun(() -> {
System.out.println("PDF 处理结束后,执行下一步");
});
thenRun() 不接收上一步结果。
因为上一步本来也没有结果。
它只是表示:
上一步执行完以后,再执行这一段。
所以如果后续步骤不需要上一步的返回值,可以用 thenRun()。
但如果后续步骤需要路径、结果对象,就应该用 supplyAsync + thenApply 或 thenAccept。
默认线程池的问题
无论是:
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()。