1. Thread.interrupt() 与协作式取消
在真实应用中,任务可能很耗时,有时甚至会“卡住”——例如处理网络、文件或外部服务时。用户可能会取消操作,服务器可能会中止请求处理,或者整体超时已到。如果不会正确取消任务,应用就会卡顿、无谓消耗资源,并且对外部事件反应迟缓。
关键思想:取消应当是协作式的——任务需要自行检查是否被要求结束,并正确释放资源。
Thread.interrupt() 如何工作
每个线程都有一个“中断”标志。当你调用 thread.interrupt() 时,这个标志会被设置为 true。线程并不会因此被“杀死”,它需要自行检查状态并结束:周期性调用 Thread.currentThread().isInterrupted(),然后正确退出。
示例:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务…
try {
Thread.sleep(100); // 可被中断
} catch (InterruptedException e) {
// 异常会清除中断标志,但可以再次设置自我中断
Thread.currentThread().interrupt();
break;
}
}
System.out.println("线程因中断而结束。");
});
worker.start();
// ... 后续
worker.interrupt();
哪些地方会自动响应中断标志?
- 可能阻塞的方法(sleep、wait、join、阻塞数据结构的操作)在中断时会抛出 InterruptedException。
- 在其他情况下(例如计算循环中),需要手动检查 isInterrupted()。
模式:“设置标志——尽快退出”
- 在调用方代码中:thread.interrupt()
- 在任务中:周期性检查 Thread.currentThread().isInterrupted()
- 必要时——正确释放资源并结束。
常见误区:期待 interrupt() 能立刻“杀死”线程。并不会——它只是一个信号,任务必须自行响应。
2. Future.cancel()、CancellationException 与任务取消
Future.cancel 如何工作
当你通过 ExecutorService.submit() 启动任务时,会得到一个 Future 对象。它有方法 cancel(boolean mayInterruptIfRunning):
- 如果任务尚未开始——将不会被启动。
- 如果任务已在执行且 mayInterruptIfRunning == true——会对执行该任务的线程调用 interrupt()。
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 长时间运行的工作
}
System.out.println("任务因取消而结束。");
});
// ... 后续
future.cancel(true); // 请求取消任务
任务实际会发生什么
通过 Future 进行取消并不是魔法般的“杀死线程”按钮,实质上就是一种更为礼貌的 Thread.interrupt()。如果任务能正确检查中断标志——它会平滑结束;如果不能——它会继续运行直到自然结束。
如果在取消后调用 future.get(),你会得到 CancellationException——提醒该任务已被取消。
3. CompletableFuture:取消、超时与链式调用
取消 CompletableFuture
CompletableFuture 同样有 cancel(boolean)。如果任务尚未完成,它会被取消,后续所有处理器(如 thenApply、thenAccept 等)都不会被调用。
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务…
}
System.out.println("CF 因取消而结束。");
});
// ... 后续
cf.cancel(true);
超时:orTimeout 与 completeOnTimeout
- orTimeout(timeout, unit)——如果未能及时完成,则以 TimeoutException 结束 CompletableFuture。
- completeOnTimeout(value, timeout, unit)——如果未能及时完成,则以指定值完成。
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {}
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "TIMEOUT")
.thenAccept(System.out::println); // 2 秒后输出: "TIMEOUT"
在链中传播取消
如果取消“上游”的 CompletableFuture,后续步骤将不会被调用。但在使用 thenCompose 启动内部异步操作时,取消并不会自动向“上游”传播——需要显式设计(检查状态、取消子任务、使用统一截止时间等)。
谨慎对待 thenCompose 与自定义 Executor!确保内部任务能够响应中断/取消,且/或能够获得统一的超时设置。
4. StructuredTaskScope:成组任务的取消
结构化并发与取消
StructuredTaskScope(Java 21+)允许启动一组任务,并将其生命周期作为一个整体进行管理。如果其中某个任务出错或超时——其余任务会被自动取消。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 若任一失败则抛出异常
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
- 如果任一任务失败——scope 会取消其余所有任务。
- 如果超时(通过 scope.joinUntil(deadline))——scope 会取消所有任务。
终止策略
- ShutdownOnFailure——在第一个失败出现时取消所有任务。
- ShutdownOnSuccess——一旦有一个成功完成,就取消其余任务。
5. 实践:安全取消耗时操作
示例:取消阻塞 IO
如果任务在读取文件或网络时被阻塞,中断线程并不总是奏效——某些 IO 操作不会对 interrupt 作出响应。在现代 API(NIO、AsynchronousFileChannel)中对中断的支持更好,但仍非处处如此。
建议:
- 若需要可取消性,尽量使用非阻塞 IO。
- 对于阻塞 IO——在 API 层设置超时(例如 Socket.setSoTimeout)。
- 对于异步任务——使用 Future.cancel,并正确响应中断。
示例:取消队列/屏障的等待
许多同步器(BlockingQueue.take()、CountDownLatch.await()、CyclicBarrier.await())在中断时会抛出 InterruptedException。在处理逻辑中捕获异常,必要时恢复中断标志,并正确结束任务。
6. 模式“time‑budget”:为一组操作设定统一截止时间
在复杂应用中,经常需要为一组操作设置统一的超时时间。例如,若用户最多等待 2 秒,而内部需要发起 3 个网络请求——它们都必须满足统一的截止时间。
如何把截止时间向下传递?
- 向所有可能阻塞的方法传递截止时间对象(例如,Instant 类型的 deadline)。
- 在每个方法中计算剩余时间:Duration.between(Instant.now(), deadline)。
- 将该时间用于阻塞操作的超时(await(timeout)、poll(timeout)、orTimeout(timeout))。
Instant deadline = Instant.now().plusSeconds(2);
void doWork(Instant deadline) throws TimeoutException, InterruptedException {
Duration left = Duration.between(Instant.now(), deadline);
if (left.isNegative() || left.isZero()) throw new TimeoutException();
// 使用 left 作为超时时间
queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}
Scoped Values / 上下文
在 Java 21+ 中可以使用 Scoped Values 将截止时间沿调用栈传递,而无需在每个方法中显式传参。
7. Structured Concurrency:在失败/超时时取消整个 scope
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
if (!completed) {
scope.shutdown();
throw new TimeoutException("截止时间已到!");
}
scope.throwIfFailed();
// ...
}
- 如果截止时间已到——scope 会取消所有任务。
- 如果某个任务失败——其余任务会被自动取消。
8. 处理取消与超时时的常见错误
错误 1:期望 interrupt() 能立刻结束线程。 实际上它只是一个信号——任务需要自行检查状态并正确结束。
错误 2:在长循环中不检查 isInterrupted()。 如果不检查中断标志,即使被要求结束,任务也会一直运行下去。
错误 3: Future.cancel() 并不会导致取消,如果任务不响应中断。 如果任务对中断“充耳不闻”,cancel() 也无济于事。
错误 4:未将超时向下传递到调用栈。 如果不在所有方法中传递截止时间,内部操作可能“挂住”得比需要的更久。
错误 5:在 thenCompose 链中, CompletableFuture 的取消不会自动传播。 如果取消“上游” future,内部任务可能仍继续运行——需要显式处理取消。
错误 6: StructuredTaskScope 未关闭(没有使用 try‑with‑resources)。 如果不关闭 scope,子任务可能会“悬挂”不退出。
GO TO FULL VERSION