啊,Java,我的老朋友。你又来了——又一次试图用“我们这次真的简化了异步编程”这种话来哄我开心。你说:“来吧,看看 Project Loom 和 Virtual Threads,它们多轻量、多丝滑!” 我点点头,感动得差点热泪盈眶——毕竟,我等这一天已经等得像等前任回心转意那么久了。
然后你递给我 JEP 453:Structured Concurrency(结构化并发),满脸期待地问:“喜欢吗?”
我看着它,沉默三秒,缓缓吐出两个字:“笨拙。”
不是说它不好。它像一个穿着西装、打着领带、试图显得很专业的高中生——有潜力,但动作总差那么点火候。你想让我相信这是“简化异步编程”的终极答案?兄弟,你怕是忘了我们开发者最怕什么:不是复杂,而是复杂的复杂。
让我用一个生活化的比喻来解释。假设你要做一顿饭:煮饭、炒菜、煲汤。理想情况下,这三件事可以同时进行,毕竟你有两只手,还有微波炉(以及日益增长的中年焦虑)。但在旧版 Java 里,你得一个一个来:
java
Rice rice = cookRice(); // 等30分钟
Vegetable stirFry = stirFry(); // 再等20分钟
Soup soup = boilSoup(); // 又等45分钟
结果你饿死在厨房门口。
而 Go 语言的做法呢?它说:“兄弟,别慌。” 于是你优雅地写下:
go
rice, stirFry, soup := go cookRice(), go stirFry(), go boilSoup()
然后你躺在沙发上刷抖音,等菜好了再起来摆盘。这才是并发的浪漫。
而 Java 的结构化并发呢?它说:“来,我给你一个 StructuredTaskScope,你要 fork(),然后 join(),别忘了 throwIfFailed(),哦对了,不能调两次 join(),不然会炸……”
我:???
这哪是做饭,这是在拆炸弹。
看看这个“简洁”的 Java 代码:
java
try (var scope = StructuredTaskScope.open()) {
Subtask rice = scope.fork(() -> cookRice());
Subtask stirFry = scope.fork(() -> stirFry());
scope.join();
return new Meal(rice.get(), stirFry.get());
} catch (FailedException e) {
log.error("做饭失败", e);
}
兄弟,我只是想吃顿饭,不是要考 Java 并发高级工程师认证!
更离谱的是,你必须手动调 join(),而且它还不是幂等的!调两次?直接抛异常。这就像你按了微波炉的“开始”键,食物还没好,你再按一次——结果微波炉自爆了。这合理吗?
还有,get() 必须在 join() 之后调用?这就像你点外卖,骑手还没到楼下,你就冲下楼去拿,然后系统说:“你违规了,封号三天。”
更让我抓狂的是,这个 API 想一统江湖——既要支持“所有任务都成功”(all-successful),又要支持“任一任务成功就行”(race semantics)。这就像你设计一把瑞士军刀,既要能开红酒瓶,又要能发射导弹。
结果呢?刀片不够锋利,导弹还哑火。
比如,anySuccessfulResultOrThrow() 这个方法,看起来很美:
java
try (var scope = open(Joiner.anySuccessfulResultOrThrow())) {
scope.fork(() -> callPrimaryBackend());
scope.fork(() -> callBackupBackend());
return scope.join();
}
简洁?是的。但问题是:它吞掉了所有异常,包括 NullPointerException、OutOfMemoryError 这种“服务器快炸了”的致命错误。
你让一个 NullPointerException 被静默吞掉,然后返回另一个后端的结果?那我是不是也可以在体检报告上把癌症划掉,写“另一个器官还行”?
现实世界中,关键错误就应该快速失败,而不是假装没事发生。否则 QA 环境里一切正常,生产环境炸成烟花,开发团队集体去庙里拜佛求别背锅。
而我自己写个
mapConcurrent 版本,反而更灵活、更可控:
java
public static T raceRpcs(int maxConcurrency, Collection> tasks) {
ConcurrentLinkedQueue suppressed = new ConcurrentLinkedQueue<>();
return tasks.stream()
.gather(mapConcurrent(maxConcurrency, task -> {
try {
return task.call();
} catch (RpcException e) {
suppressed.add(e);
return null;
}
}))
.filter(Objects::nonNull)
.findAny()
.orElseThrow(() -> propagate(suppressed));
}
看,我能精准控制哪些异常要吞,哪些要炸。这叫优雅的失败,而不是“假装世界很美好”。
更让我困惑的是,API 的使用模式还不一致:
- “所有成功”:你得保存 Subtask,join() 后再 .get()。
- “任一成功”:你直接从 join() 拿结果,Subtask 成了 disposable 初恋。
这就像同一个恋爱软件,有些人靠聊天确定关系,有些人直接领证——没有统一的情感流程,用户容易精神分裂。
所以我在想:能不能别贪心?
90% 的场景只是想并发执行几个独立任务,最后合并结果。就这么简单的需求,为什么非要塞进一个复杂的 StructuredTaskScope 里?
我梦想中的 API 是这样的:
java
Meal meal = concurrently(
() -> cookRice(),
() -> stirFry(),
() -> boilSoup(),
(rice, stirFry, soup) -> new Meal(rice, stirFry, soup)
);
干净、直观、无痛。像 Go,但带着 Java 的稳重(和一点点啰嗦)。
如果真要支持“竞争语义”,那就单独搞个 race() 工具方法,别污染主流程。毕竟,不是每个人都想参加百米赛跑,大多数人只想好好走路。
总结一下我对结构化并发的“爱之深,责之切”:
1. 目标正确:简化异步,告别回调地狱,让 RxJava 安详退休。
2. 实现太卷:API 复杂、规则多、容易踩坑。
3. 异常处理太佛系:关键错误被吞,生产环境埋雷。
4. 语义混杂:all-success 和 race 强行合并,结果两头不讨好。
5. 本可以更简单:为大多数场景设计,而不是为所有可能场景设计。
最后,我想对 JDK 团队说:你们辛苦了,我知道设计通用 API 很难。但有时候,少就是多。别总想着造一辆能上天入地的超级战车,有时候,用户只想要一辆能顺利开到公司不抛锚的自行车。
而我?我还是会继续用 concurrently 的梦想写注释,然后在生产环境里默默用 CompletableFuture 和 mapConcurrent 过日子。
毕竟,在 Java 的世界里,希望就像虚拟线程——轻量,但不一定真的自由。
——来自一个被异步折磨到怀疑人生,但仍相信明天会更好的 Java 程序员。