1. 启动异步任务:supplyAsync 和 runAsync
最常见的启动异步任务的方式是使用 CompletableFuture.supplyAsync。该方法接收一个返回结果的 lambda 或方法。例如,我们想要模拟从服务器加载数据:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作(例如,下载文件)
sleep(1000);
return "来自服务器的数据";
});
System.out.println("任务已启动!");
// ... 在任务执行期间,这里可以先做点别的
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync:当不需要结果时
如果你的任务不返回任何结果(例如,只是写日志、发送通知),请使用 runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("操作完成!");
});
runAsync 始终返回 CompletableFuture<Void>,因为它不期望有结果。
2. thenApply、thenAccept、thenRun:有什么区别?
当异步任务结束后,我们通常希望对结果做点事情。为此提供了这些“处理器”方法:
- thenApply —— 转换结果并返回一个新的结果。
- thenAccept —— 接收结果但不返回任何值(用于产生副作用)。
- thenRun —— 不接收结果,也不返回任何值(仅在任务结束后执行一个动作)。
thenApply:处理并转换结果
当你需要转换前一个任务的结果时,使用 thenApply。例如:加载了一段字符串,现在想获取它的长度:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("正在计算字符串长度…");
return s.length();
});
// lengthFuture 现在包含 Integer —— 字符串 "Java" 的长度
lengthFuture.thenAccept(len -> System.out.println("长度:" + len));
发生了什么:
- future 含有字符串 "Java"。
- thenApply 将字符串转换为它的长度(int)。
- thenAccept 输出结果。
thenAccept:对结果执行动作(不返回值)
如果只需要对结果做点事情(比如打印到屏幕),且不需要返回值——使用 thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "你好,世界!");
future.thenAccept(result -> {
System.out.println("结果:" + result);
});
thenAccept 就像一个“消费者”:它消费结果并对其做一些有用的事。
thenRun:不关心结果的动作
如果你只想在任务完成后执行一个动作,但并不需要结果,请使用 thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "完成!");
future.thenRun(() -> {
System.out.println("加载完成!");
});
请注意:在 thenRun 内无法获取前一任务的结果——它会被忽略。
3. 调用链:把任务构建成流水线
CompletableFuture 的强大之处在于可以构建计算链。每个方法(thenApply、thenAccept、thenRun)都会返回一个新的 CompletableFuture,你可以继续在其上添加处理器。
示例:多阶段处理
让我们完善一下示例应用:先加载数据,再转换,之后输出结果,最后记录日志表示全部结束。
CompletableFuture.supplyAsync(() -> {
System.out.println("步骤 1:加载数据…");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("步骤 2:转换数据…");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("步骤 3:输出结果:" + result);
})
.thenRun(() -> {
System.out.println("步骤 4:全部完成!");
});
控制台输出:
步骤 1:加载数据…
步骤 2:转换数据…
步骤 3:输出结果:JAVA
步骤 4:全部完成!
请注意:
每一步都会在前一步完成之后才开始。这使我们能够构建真正的数据处理“流水线”。
4. 异步变体:thenApplyAsync、thenAcceptAsync、thenRunAsync
默认情况下,处理器(thenApply、thenAccept、thenRun)会在前一个任务完成所在的同一线程里执行。有时这并不理想——如果处理较重,最好将其放到其他线程中执行。
为此提供了异步版本:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
有什么不同?
- 无 Async: 处理器可能在与前一任务相同的线程中执行(例如,如果任务在 ForkJoinPool 中完成,处理器也可能在那里执行)。
- 有 Async: 处理器将保证在 ForkJoinPool 的其他线程中执行(或在你的 Executor 中)。
示例:比较普通处理器与异步处理器
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("加载中... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
典型输出:
加载中... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
结论:
异步处理器会在另一条线程中执行。
何时使用 Async 方法?
- 当处理较为耗资源(例如复杂计算、网络 I/O)。
- 当你不希望阻塞前一任务结束所在的线程。
- 当你希望显式管理线程(例如,传入自定义的 Executor 作为第二个参数)。
5. 有用的细节
表:thenApply、thenAccept、thenRun 方法对比
| 方法 | 是否使用结果? | 是否返回值? | 用途 |
|---|---|---|---|
|
是 | 是 | 结果转换 |
|
是 | 否 | 副作用(输出、日志) |
|
否 | 否 | 仅在任务结束后执行一个动作 |
|
是 | 是 | 相同,但在另一条线程中 |
|
是 | 否 | 相同,但在另一条线程中 |
|
否 | 否 | 相同,但在另一条线程中 |
问:如何构建更长的链?
可以像搭 LEGO 一样一个接一个地调用方法:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("结果:" + x));
输出:
结果:84
每一步都会接收上一步的结果,可以对其进行修改或仅仅使用它。
6. 使用 thenApply、thenAccept、thenRun 时的常见错误
错误 1:对返回值类型的混淆。
thenApply 必须返回一个值以便传递给链中的后续步骤。如果你误用了 thenApply 却没有返回结果,下一步可能会收到 null(或者根本无法通过编译)。对于副作用,请使用 thenAccept 或 thenRun。
错误 2:试图在 thenRun 中使用结果。
在 thenRun 内无法访问前一任务的结果。如果你需要使用结果,请选择 thenApply 或 thenAccept。
错误 3:阻塞主线程。
如果你在主线程中调用 get() 或 join(),就会失去异步的优势:线程会等待任务结束,像传统的同步代码一样。更好的做法是使用非阻塞的调用链与回调。
错误 4:未处理异常。
如果链中发生了异常,而你没有添加处理器(exceptionally、handle、whenComplete),异常可能会“悄然消失”,任务也许以错误结束而你却看不到。请务必在调用链中处理异常。
错误 5:意外地在其他线程执行。
异步方法(如 thenApplyAsync 等)可能在另一条线程中执行。如果你访问了未做并发保护的变量,可能会发生数据竞争。
GO TO FULL VERSION