CodeGym /Kurse /JAVA 25 SELF /Abbruch von Aufgaben und Timeouts über den Stack

Abbruch von Aufgaben und Timeouts über den Stack

JAVA 25 SELF
Level 58 , Lektion 1
Verfügbar

1. Thread.interrupt() und kooperativer Abbruch

In realen Anwendungen können Aufgaben lange dauern und manchmal sogar „hängen bleiben“ – etwa bei der Arbeit mit Netzwerk, Dateien oder externen Diensten. Der Benutzer kann eine Operation abbrechen, der Server – die Verarbeitung einer Anfrage beenden, oder ein gemeinsamer Timeout läuft einfach ab. Wenn man Aufgaben nicht korrekt abbrechen kann, hängt die Anwendung, verschwendet Ressourcen und reagiert schlecht auf externe Ereignisse.

Schlüsselidee: Der Abbruch muss kooperativ sein – die Aufgabe soll selbst prüfen, ob sie um Beendigung gebeten wurde, und Ressourcen sauber freigeben.

Wie Thread.interrupt() funktioniert

Jeder Thread hat ein „Interrupt“-Flag. Wenn Sie thread.interrupt() aufrufen, wird dieses Flag auf true gesetzt. Der Thread wird dabei nicht „abgeschossen“, er muss seinen Status selbst prüfen und sich beenden: regelmäßig Thread.currentThread().isInterrupted() aufrufen und sauber aussteigen.

Beispiel:

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Wir arbeiten ...
        try {
            Thread.sleep(100); // Kann unterbrochen werden
        } catch (InterruptedException e) {
            // Das Flag wird zurückgesetzt, aber wir können uns erneut unterbrechen
            Thread.currentThread().interrupt();
            break;
        }
    }
    System.out.println("Thread wurde durch Unterbrechung beendet.");
});
worker.start();

// ... später
worker.interrupt();

Wo wirkt das Flag automatisch?

  • Methoden, die blockieren können (sleep, wait, join, Operationen blockierender Strukturen), werfen bei einer Unterbrechung InterruptedException.
  • In allen anderen Fällen (z. B. in einer Rechenschleife) müssen Sie isInterrupted() manuell prüfen.

Muster „Flag setzen – und zügig beenden“

  1. Im aufrufenden Code: thread.interrupt()
  2. In der Aufgabe: regelmäßig Thread.currentThread().isInterrupted() prüfen
  3. Bei Bedarf – Ressourcen sauber freigeben und beenden.

Typischer Fehler: zu erwarten, dass interrupt() den Thread sofort „tötet“. Nein – es ist nur ein Signal; die Aufgabe muss selbst reagieren.

2. Future.cancel(), CancellationException und Aufgabenabbruch

Wie Future.cancel funktioniert

Wenn Sie eine Aufgabe über ExecutorService.submit() starten, erhalten Sie ein Objekt Future. Es besitzt die Methode cancel(boolean mayInterruptIfRunning):

  • Wenn die Aufgabe noch nicht gestartet wurde – wird sie nicht mehr gestartet.
  • Wenn die Aufgabe bereits läuft und mayInterruptIfRunning == true ist – wird interrupt() beim Thread aufgerufen, der die Aufgabe ausführt.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Lange Arbeit
    }
    System.out.println("Aufgabe wurde durch Abbruch beendet.");
});

// ... später
future.cancel(true); // Wir bitten um Abbruch der Aufgabe

Was tatsächlich mit der Aufgabe passiert

Der Abbruch über Future ist kein magischer „Thread‑Kill‑Schalter“, sondern im Grunde eine höfliche Form von Thread.interrupt(). Prüft die Aufgabe das Interrupt-Flag korrekt – beendet sie sich ordentlich. Tut sie das nicht – läuft sie bis zum natürlichen Ende weiter.

Wenn Sie future.get() nach dem Abbruch aufrufen, erhalten Sie eine CancellationException – eine Erinnerung daran, dass die Aufgabe abgebrochen wurde.

3. CompletableFuture: Abbruch, Timeouts und Ketten

Abbruch von CompletableFuture

Auch CompletableFuture hat cancel(boolean). Wenn die Aufgabe noch nicht abgeschlossen ist, wird sie abgebrochen, und alle weiteren Handler (thenApply, thenAccept usw.) werden nicht aufgerufen.

CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Wir arbeiten ...
    }
    System.out.println("CF wurde durch Abbruch beendet.");
});

// ... später
cf.cancel(true);

Timeouts: orTimeout und completeOnTimeout

  • orTimeout(timeout, unit) – beendet CompletableFuture mit TimeoutException, wenn es nicht rechtzeitig fertig wird.
  • completeOnTimeout(value, timeout, unit) – beendet es mit dem angegebenen Wert, wenn es nicht rechtzeitig fertig wird.
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); // Nach 2 Sekunden: "TIMEOUT"

Abbruch in Ketten weiterreichen

Wenn man das „oberste“ CompletableFuture abbricht, werden alle nachfolgenden Schritte in der Kette nicht ausgeführt. Beim Einsatz von thenCompose zum Starten interner asynchroner Operationen wird der Abbruch jedoch nicht automatisch „nach oben“ propagiert – man muss ihn explizit gestalten (Status prüfen, Kindaufgaben abbrechen, eine gemeinsame Deadline verwenden).

Vorsicht mit thenCompose und einem benutzerdefinierten Executor! Stellen Sie sicher, dass interne Aufgaben auf Unterbrechung/Abbruch reagieren können und/oder eine gemeinsame Timeout-Deadline erhalten.

4. StructuredTaskScope: Abbruch einer Aufgabengruppe

Structured Concurrency und Abbruch

StructuredTaskScope (Java 21+) ermöglicht es, eine Gruppe von Aufgaben zu starten und ihren Lebenszyklus als Ganzes zu verwalten. Wenn eine der Aufgaben mit einem Fehler endet oder ein Timeout abläuft – werden die übrigen Aufgaben automatisch abgebrochen.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchData1());
    Future<String> f2 = scope.fork(() -> fetchData2());

    scope.join(); // wir warten auf den Abschluss aller Aufgaben
    scope.throwIfFailed(); // wenn mindestens eine fehlgeschlagen ist – werfen wir eine Exception

    String result = f1.resultNow() + f2.resultNow();
    System.out.println(result);
}
  • Wenn irgendeine Aufgabe mit Fehler endet – bricht der Scope alle übrigen Aufgaben ab.
  • Wenn das Timeout abläuft (über scope.joinUntil(deadline)) – bricht der Scope alle Aufgaben ab.

Abschlussrichtlinien

  • ShutdownOnFailure – bricht alle Aufgaben beim ersten Fehler ab.
  • ShutdownOnSuccess – bricht die übrigen Aufgaben ab, sobald eine erfolgreich abgeschlossen wurde.

5. Praxis: sicherer Abbruch lang laufender Operationen

Beispiel: Abbruch von blockierendem I/O

Wenn eine Aufgabe beim Lesen aus Datei oder Netzwerk blockiert, hilft das Unterbrechen des Threads nicht immer – einige I/O‑Operationen reagieren nicht auf interrupt. In modernen APIs (NIO, AsynchronousFileChannel) wird das Unterbrechen besser unterstützt, aber noch immer nicht überall.

Empfehlungen:

  • Verwenden Sie nicht-blockierendes I/O, wenn Abbruch erforderlich ist.
  • Für blockierendes I/O – setzen Sie Timeouts auf API‑Ebene (z. B. Socket.setSoTimeout).
  • Für asynchrone Aufgaben – verwenden Sie Future.cancel und reagieren Sie korrekt auf Unterbrechung.

Beispiel: Abbruch beim Warten auf Queue/Barriere

Viele Synchronisierer (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) werfen bei Unterbrechung eine InterruptedException. Fangen Sie die Ausnahme im Handler, setzen Sie das Flag bei Bedarf wieder und beenden Sie die Aufgabe sauber.

6. Pattern „Time‑Budget“: gemeinsame Deadline für eine Gruppe von Operationen

In komplexen Anwendungen muss man oft einen gemeinsamen Timeout für eine Gruppe von Operationen vorgeben. Wenn der Benutzer beispielsweise höchstens 2 Sekunden auf eine Antwort wartet und intern 3 Netzwerkanfragen ausgeführt werden sollen – müssen alle in die gemeinsame Deadline passen.

Wie reicht man die Deadline nach unten durch den Stack weiter?

  • Geben Sie ein Deadline‑Objekt (z. B. Instant deadline) an alle potenziell blockierenden Methoden weiter.
  • Berechnen Sie in jeder Methode die verbleibende Zeit: Duration.between(Instant.now(), deadline).
  • Verwenden Sie diese Zeit für Timeouts in blockierenden Operationen (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();
    // Wir verwenden left für den Timeout
    queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}

Scoped Values / Kontext

In Java 21+ können Scoped Values verwendet werden, um die Deadline entlang des Aufrufstapels weiterzugeben, ohne sie explizit an jede Methode zu übergeben.

7. Structured Concurrency: gesamten Scope bei Fehler/Timeout abbrechen

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("Deadline abgelaufen!");
    }
    scope.throwIfFailed();
    // ...
}
  • Wenn die Deadline abläuft – bricht der Scope alle Aufgaben ab.
  • Wenn eine Aufgabe fehlschlägt – werden die übrigen automatisch abgebrochen.

8. Typische Fehler im Umgang mit Abbruch und Timeouts

Fehler Nr. 1: Erwarten, dass interrupt() den Thread sofort beendet. In Wirklichkeit ist es nur ein Signal – die Aufgabe muss den Status selbst prüfen und sich sauber beenden.

Fehler Nr. 2: isInterrupted() wird in langen Schleifen nicht geprüft. Wenn das Interrupt‑Flag nicht geprüft wird, läuft die Aufgabe ewig weiter, selbst wenn um Beendigung gebeten wurde.

Fehler Nr. 3: Future.cancel() führt nicht zum Abbruch, wenn die Aufgabe nicht auf interrupt reagiert. Ist die Aufgabe „taub“, hilft cancel() nicht.

Fehler Nr. 4: Timeouts werden nicht nach unten durch den Stack propagiert. Wenn die Deadline nicht an alle Methoden weitergegeben wird, kann eine interne Operation länger „hängen“ als nötig.

Fehler Nr. 5: In thenCompose ‑Ketten von CompletableFuture wird der Abbruch nicht automatisch propagiert. Wenn das „oberste“ Future abgebrochen wird, können interne Aufgaben weiterlaufen – behandeln Sie den Abbruch explizit.

Fehler Nr. 6: StructuredTaskScope wird nicht geschlossen (kein try‑with‑resources). Wenn der Scope nicht geschlossen wird, können Kindaufgaben „hängen bleiben“.

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