Administrator
发布于 2023-05-25 / 7339 阅读
196

结构化并发 Structured Concurrency 实践

面试被问住:线程池里的异常去哪了

五月面试一个候选人,他反过来问我:「用 ExecutorService 提交了三个任务,其中一个抛异常,另外两个还在跑,怎么保证它们被取消?」我脱口而出「写个 Future 判断」,但细想发现,传统线程池里任务生命周期是散的,取消一个不影响其他——这恰恰是结构化并发要解决的问题。

传统写法的痛点

假设要并发查「用户信息 + 订单 + 推荐」,传统代码:

var f1 = svc.submit(() -> getUser());
var f2 = svc.submit(() -> getOrder());
var f3 = svc.submit(() -> getRecommend());
// 任一个失败,另外两个还在后台空跑,资源泄漏
User u = f1.get();
Order o = f2.get();   // 这里抛异常
Order r = f3.get();

f2.get() 抛异常后,f3 对应的任务不会自动停,线程白白占着。这在高并发下会悄悄拖垮线程池。

StructuredTaskScope:把任务关进同一作用域

JDK 21 预览的 StructuredTaskScope(当时还是 incubator,我用 EA build 19 验证)把一组子任务绑定到父线程的作用域上,父作用域结束,所有子任务必须全部完结或被取消。

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var u = scope.fork(() -> getUser());
    var o = scope.fork(() -> getOrder());
    var r = scope.fork(() -> getRecommend());

    scope.join();          // 等所有子任务
    scope.throwIfFailed(); // 任一失败,抛第一个异常

    return new Result(u.get(), o.get(), r.get());
}

任务生命周期管理

  • ShutdownOnFailure:任一子任务失败,立即取消其余任务——适合「全有或全无」的聚合查询。
  • ShutdownOnSuccess:任一成功就取消其余——适合「取最快响应」的场景,比如多机房竞速。

关键在 try-with-resources:作用域结束(无论正常还是异常),所有 fork 出去的任务都被强制 join + 取消,不会留下孤儿线程

错误传播与取消

throwIfFailed() 会把子任务的异常原样抛给父线程(用 ExecutionException 包裹),不会吞掉。取消是通过线程的 interrupt 信号传递的,所以子任务里如果有可中断的阻塞(Thread.sleepBlockingQueue.take),会立刻响应。

实测:三个任务各 500ms,其中一个 100ms 就失败,使用结构化的版本整体 100ms 就返回并清掉另外俩;传统写法要等 500ms 且残留线程。

小结

结构化并发把「任务树」的概念带回 Java——子任务的生命周期被父作用域接管,错误向上传播、失败自动取消。它目前还是预览 API,且依赖虚拟线程才能发挥最大价值(fork 的代价足够低)。但那种「任务池散落、异常难追踪」的老问题,它给出了干净的范式级答案。

参考