CodeGym /課程 /JAVA 25 SELF /沿著堆疊傳遞的任務取消與逾時

沿著堆疊傳遞的任務取消與逾時

JAVA 25 SELF
等級 58 , 課堂 1
開放

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();

// ... later
worker.interrupt();

哪些情況會自動處理中斷旗標?

  • 可能阻塞的方法(sleepwaitjoin、以及阻塞資料結構的操作)在中斷時會拋出 InterruptedException
  • 其他情況(例如計算迴圈)需要手動檢查 isInterrupted()

模式:「設定旗標——儘快退出」

  1. 在呼叫端:thread.interrupt()
  2. 在任務中:定期檢查 Thread.currentThread().isInterrupted()
  3. 必要時——妥善釋放資源並結束。

常見錯誤:期待 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("任務因取消而結束。");
});

// ... later
future.cancel(true); // 請求取消任務

任務實際上發生了什麼

透過 Future 的取消並不是一個「殺死執行緒」的魔法按鈕,本質上是有禮貌地呼叫 Thread.interrupt()。如果任務能正確檢查中斷旗標——它會平順地結束。否則——會繼續執行直到自然完成。

在取消之後呼叫 future.get(),你會得到 CancellationException——提醒你該任務已被取消。

3. CompletableFuture:取消、逾時與鏈結

取消 CompletableFuture

CompletableFuture 也有 cancel(boolean)。如果任務尚未完成,它會被取消,之後的所有處理器(如 thenApplythenAccept 等)都不會被呼叫。

CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 執行中...
    }
    System.out.println("CF 因取消而結束。");
});

// ... later
cf.cancel(true);

逾時:orTimeoutcompleteOnTimeout

  • 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:成組任務的取消

Structured Concurrency 與取消

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(如 NIOAsynchronousFileChannel)中,中斷支援較好,但仍非處處可用。

建議:

  • 如果需要取消,盡量使用非阻塞 IO。
  • 對於阻塞 IO——在 API 層級設定逾時(例如 Socket.setSoTimeout)。
  • 對於非同步任務——使用 Future.cancel,並正確回應中斷。

範例:取消對佇列/柵欄的等待

許多同步器(BlockingQueue.take()CountDownLatch.await()CyclicBarrier.await())在中斷時會拋出 InterruptedException。在處理時捕捉例外,必要時恢復旗標,並正確結束任務。

6. 模式「time‑budget」:一組操作的共同截止時間

在複雜的應用中,常需要為一組操作設定共同的逾時。例如使用者只願意等待 2 秒、但內部需要進行 3 個網路請求——它們都必須符合共同的截止時間。

如何沿著呼叫堆疊向下傳遞截止時間?

  • 將截止時間物件(例如 Instantdeadline)傳入所有可能阻塞的方法。
  • 在每個方法中計算剩餘時間: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,子任務可能會持續「懸掛」。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION