
Agent 超时后调用 future.cancel(true),返回 true,接着把订单状态改成“执行失败”。这条链路藏着一个误判:Future 的取消状态,描述的是结果句柄;工作线程有没有响应中断、远端有没有提交,是另外两件事。
这个问题不会因为换成虚拟线程自动消失。Java 24 的 StructuredTaskScope 仍是预览 API,需要相应编译和运行选项。其文档区分得很清楚:shutdown 会中断未完成子任务,但不等待它们停止;close 会等待剩余线程结束。joinUntil 的截止时间因此不能直接解释成整个作用域的物理退出上限。
一个不依赖虚拟线程的反例
以下代码故意让工作者忽略中断,直到测试释放门闩。它不是推荐的业务写法,而是用来证明“取消成功”不等于“副作用已阻止”。保存为 CancelBoundary.java,运行 java CancelBoundary.java。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CancelBoundary {
static void check(boolean value, String reason) {
if (!value) throw new AssertionError(reason);
}
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch finished = new CountDownLatch(1);
AtomicInteger committed = new AtomicInteger();
Future<?> f = pool.submit(() -> {
started.countDown();
boolean released = false;
while (!released) {
try {
release.await();
released = true;
} catch (InterruptedException ignored) {
// 仅用于反例:实际业务不应照搬这种吞掉中断的循环。
}
}
committed.incrementAndGet();
finished.countDown();
});
try {
check(started.await(2, TimeUnit.SECONDS), "worker not started");
check(f.cancel(true), "future was not cancelled");
try {
f.get();
throw new AssertionError("cancelled result must not be returned");
} catch (CancellationException expected) {
check(committed.get() == 0, "gate must still block action");
}
release.countDown();
check(finished.await(2, TimeUnit.SECONDS), "worker did not finish");
check(committed.get() == 1, "action can occur after cancellation");
System.out.println("cancelled handle; worker action still occurred");
} finally {
release.countDown();
pool.shutdownNow();
check(pool.awaitTermination(2, TimeUnit.SECONDS), "pool still running");
}
}
}

这个例子在本地 JDK 12 运行通过,没有启动 Java 24 虚拟线程,没有数据库事务,也没有退款请求。计数器仅代表一个可观察动作。测试中的门闩保证事件顺序,避免靠 sleep 猜测线程何时执行。
Agent 应怎样表达结果
只读搜索超时,可以说明本轮没有取得结果。退款、扣款、发消息这类动作超时,如果没有权威回执,应进入“结果待核实”,而不是立刻重新执行或宣布失败。后续用稳定业务键查询状态;真正重试前,还需要服务端幂等语义。
在线程内部,捕获 InterruptedException 后通常应退出、向上抛出,或在无法立即退出时恢复中断标记并按协议收尾;不能把中断当作撤销外部事务的机制。对关闭阶段也要设计监控和隔离预算:如果底层库不响应中断,结构化并发提供的生命周期约束可能表现为调用者继续等待。
虚拟线程降低的是某些阻塞并发的承载成本。它不替我们定义“什么时候可以说这笔业务没有发生”。
参考:Java 24 StructuredTaskScope 的 shutdown、joinUntil 与 close。


