Praktische Best Practices für Multithreading: Java-Edition — Konventionen für das Zeitalter der virtuellen Threads

· · Multithreading, Java, Geschäftsanwendungen, Fehleruntersuchung, Design

„Wir möchten einen Batch-Job in einem Geschäftssystem mit Java parallelisieren.“ „Der gemeinsame Cache in unserer Spring-Webanwendung geht gelegentlich kaputt.“ „Wir haben eine alte Swing-Anwendung übernommen, die vor new Thread nur so strotzt.“ — Java hat Multithreading seit JDK 1.0 fest in die Sprache eingebaut, ist mit java.util.concurrent zu einem ausgereiften Werkzeugkasten herangewachsen und hat mit den virtuellen Threads in JDK 21 die gängigen Vorstellungen von nebenläufiger Programmierung noch einmal neu geschrieben. Gerade weil das Werkzeugrepertoire so reichhaltig ist, wird die Entscheidung, welches Werkzeug man wählt, selbst zur Entwurfsqualität.

Dieser Artikel ist die Java-Edition der Multithreading-Praxisserie. Er richtet sich an Entwickler, die in Java Geschäftssysteme, Batch-Jobs und Serveranwendungen schreiben, überträgt die Entwurfsprinzipien des Multithreadings — Threads nicht direkt erstellen, gemeinsam genutzten veränderlichen Zustand reduzieren, Sperrdisziplin, zuerst entwerfen, wie es angehalten wird — auf die Werkzeuge von Java (im Wesentlichen die LTS-Version JDK 21 und neuer) und fasst dabei die Unterscheidungen des Zeitalters der virtuellen Threads sowie javaspezifische Fallstricke zusammen, gestützt auf Primärquellen mit Stand August 2026. Er ist so geschrieben, dass er auch für sich allein gelesen werden kann. Dieselben Prinzipien, auf andere Sprachen übertragen, finden Sie in der „.NET-Edition“, der „C++-Edition“ und der „C-Edition“.

1. Das Wichtigste zuerst

  • new Thread gehört auch in Java nicht in Geschäftscode. Geben Sie Aufgaben an ExecutorService weiter und überlassen Sie die Verwaltung des Thread-Lebenszyklus der Bibliothek.1
  • Leiten Sie überwiegend auf I/O wartende Aufgaben an virtuelle Threads. Die in JDK 21 offiziell eingeführten virtuellen Threads werden nach dem Prinzip „ein virtueller Thread pro Aufgabe“ verwendet und niemals gepoolt. Die Begrenzung der Nebenläufigkeit erfolgt nicht über einen Pool, sondern über Semaphore.23
  • Virtuelle Threads sind ein Werkzeug für den Durchsatz, kein Werkzeug, um Berechnungen zu beschleunigen. Für CPU-gebundene Parallelisierung sind wie bisher Plattform-Threads in Kernzahl-Größenordnung zuständig (fester Pool oder parallel stream).3
  • Sperren Sie über ein private final-Sperrobjekt oder ein dediziertes ReentrantLock. synchronized(this) und das Sperren eines öffentlich sichtbaren Objekts können mit externem Code kollidieren. Brauchen Sie eine zeitlich begrenzte Erlangung (tryLock), verwenden Sie ReentrantLock.
  • In JDK 21–23 gab es das Problem, dass Blockierungen innerhalb von synchronized virtuelle Threads pinnen, aber JDK 24 (JEP 491) hat das behoben. Unterscheiden Sie zwischen alten Warnhinweisen und der aktuellen Realität.34
  • volatile garantiert Sichtbarkeit und Ordnung, nicht Atomarität. Für Zähler verwenden Sie AtomicInteger / LongAdder, für zusammengesetzten Zustand eine Sperre.5
  • Kooperative Abbruchsteuerung über Interrupts ist die einzig richtige Antwort für das Stoppen. Thread.stop / suspend / resume lösen heute UnsupportedOperationException aus. Verschlucken Sie InterruptedException nicht, sondern stellen Sie den Status wieder her oder werfen Sie sie weiter.6
  • Stoppen Sie ExecutorService mit dem zweistufigen Muster „shutdown → awaitTermination → shutdownNow“. shutdownNow ist Best-Effort (in der Standardimplementierung über Interrupts), setzt also voraus, dass die Aufgaben selbst auf Interrupts reagieren.1
  • Die UI von Swing gehört ausschließlich dem EDT (Event Dispatch Thread). Von einem anderen Thread aus erfolgt der Zugriff als Auftrag über SwingUtilities.invokeLater.7

2. Warum Multithreading schwierig ist — Race Conditions, Deadlocks und das Speichermodell

Letztlich führt Multithreading, unabhängig von der Sprache, zwei Arten von Problemen ein.

Eine Race Condition ist ein Fehler, bei dem sich das Ergebnis danach richtet, in welcher Reihenfolge mehrere Threads eine bestimmte Codestelle erreichen. Das klassische Beispiel ist ein gemeinsam genutzter Zähler: Der eine Ausdruck count++ zerfällt tatsächlich in drei Schritte – „lesen → addieren → zurückschreiben“. Treten zwei Threads gleichzeitig in diese drei Schritte ein, wird die Addition des einen durch das Zurückschreiben des anderen überschrieben und geht verloren. Das Ergebnis ändert sich bei jedem Lauf, und welches Ergebnis herauskommt, ist nicht vorhersagbar.

Thread BGemeinsame Variable countThread AThread BGemeinsame Variable countThread Acount = 10count = 11 trotz zweier InkrementeDas Inkrement von Thread A ging verlorenLesen (10)Lesen (10)Lokal addieren (11)Lokal addieren (11)Zurückschreiben (11)Zurückschreiben (11)

Abbildung 1: Eine typische Race Condition, bei der ein Inkrement auf einem gemeinsam genutzten Zähler verloren geht. Unterbricht ein anderer Thread die drei Schritte von count++, überschreibt derjenige, der zuletzt zurückschreibt, den anderen

Ein Deadlock ist ein Zustand, in dem zwei Threads jeweils auf die vom anderen gehaltene Sperre warten und keiner von beiden weiterkommt. Thread A hält Sperre 1 und wartet auf Sperre 2, Thread B hält Sperre 2 und wartet auf Sperre 1 — das allein genügt, damit beide für immer stehen bleiben.

wartet auf Freigabe von Sperre 2wartet auf Freigabe von Sperre 1Thread Ahält Sperre 1Thread Bhält Sperre 2

Abbildung 2: Die zirkuläre Wartesituation eines Deadlocks. Sobald die Wartepfeile einen Ring bilden, bleiben alle Threads in diesem Ring für immer stehen

Beides ist zeitabhängig: Eine Ausführungsreihenfolge, die auf einer Entwicklungsmaschine nur einmal unter Zehntausenden von Läufen zutrifft, kann auf einem Produktionsserver mit anderer Kernzahl und anderer Last täglich auftreten. Auch „es reproduziert sich nicht, wenn man den Debugger anhängt“ oder „es verschwand, als ich Logging hinzufügte“ liegt daran, dass die Beobachtung selbst das Timing verändert – ein typisches Verhalten von Race-Bugs. Genau deshalb weisen alle Prinzipien dieses Artikels in eine Richtung: Vor „richtig synchronisieren“ steht „die Stellen reduzieren, an denen synchronisiert werden muss“.

2.1. Eine javaspezifische Grundlage — das Speichermodell und happens-before

Javaspezifisch kommt hinzu, dass die Sichtbarkeit gemeinsam genutzter Daten durch die happens-before-Beziehungen des Java Memory Model (JMM) definiert wird.

Lesen und Schreiben einer gemeinsam genutzten Variable ohne Synchronisierung führt zwar nicht zu „undefiniertem Verhalten“ wie in C++, aber es kann rechtmäßig dazu kommen, dass veraltete Werte weiterhin sichtbar bleiben oder die Reihenfolge der Schreibvorgänge vertauscht erscheint. Eine Speicherkonsistenzfehler, bei dem „eine Schleife ein boolean-Flag beobachtet, der von einem anderen Thread geänderte Wert aber nie sichtbar wird“, ist ein vom JMM erlaubtes Verhalten, kein Fehler der JVM.5 Die Werkzeuge, die davor schützen, sind die Mechanismen, die happens-before-Beziehungen erzeugen – synchronized, volatile sowie die Klassen aus java.util.concurrent. Verwenden Sie die nebenläufigen Collections korrekt, garantiert Ihnen die Bibliothek die „happens-before-Beziehung zwischen einer Aktualisierungsoperation und einem nachfolgenden Abruf“.8

Die praktische Richtlinie für Java lässt sich also so zusammenfassen: Versuchen Sie sich nicht mit einer rohen gemeinsamen Variable zu behelfen. Verwenden Sie für die gemeinsame Nutzung die Werkzeuge aus java.util.concurrent und lassen Sie happens-before von der Bibliothek herstellen.

3. Wie man Threads erstellt — ExecutorService und virtuelle Threads

3.1. Trennung von Aufgabe und Ausführung

In Java trägt ExecutorService das Prinzip „Threads nicht selbst erstellen“. Es trennt die Arbeit (Runnable / Callable) von der Frage, wie sie ausgeführt wird (mit wie vielen Threads, über welche Warteschlange), und überlässt Erstellung, Wiederverwendung und Verwerfen von Threads der Bibliothek.1

Ab JDK 21 ist die Wahl des Ausführungsmittels zu einer einfachen Zweierwahl geworden.23

Überwiegend Warten auf I/OHTTP-Aufrufe, DB, DateienCPU-gebundene BerechnungEs gibt eine Aufgabe, die nebenläufig laufen sollWas dominiert die Aufgabe?Virtuelle ThreadsExecutors.newVirtualThreadPerTaskExecutor()einer pro Aufgabe. Nicht poolenFester Pool aus Plattform-ThreadsExecutors.newFixedThreadPool(etwa Kernzahl)oder parallel streamBegrenzung gleichzeitiger Zugriffe aufexterne Dienste über Semaphore, nicht über den Pool

Abbildung 3: Die Wahl des Ausführungsmittels ab JDK 21. Ziehen Sie zuerst die Linie „bei I/O die Art des Wartens ändern, bei CPU parallelisieren“ und teilen Sie dann I/O-gebundene Arbeit virtuellen Threads und CPU-gebundene Arbeit dem klassischen Pool zu

Auf der CPU-gebundenen Seite ist ein Vorbehalt zu beachten. Executors.newFixedThreadPool begrenzt zwar die Thread-Anzahl, aber die Warteschlange ist unbegrenzt. Bei einem dauerhaft laufenden Dienst, bei dem der Zulauf die Verarbeitung ständig übersteigt, ist nur die Thread-Anzahl auf die Kernzahl begrenzt – die in der Warteschlange aufgestauten Aufgaben samt ihren Daten fressen weiterhin Speicher. Konfigurieren Sie in einem solchen Aufbau entweder direkt über ThreadPoolExecutor eine kapazitätsbegrenzte Warteschlange plus eine Ablehnungsrichtlinie, oder setzen Sie auf der einreichenden Seite eine Zugangsbeschränkung wie Semaphore ein, damit sich Backpressure erzeugen lässt (dasselbe Prinzip wie bei der Warteschlange in Kapitel 4).

3.2. Virtuelle Threads nicht falsch einsetzen

Virtuelle Threads sind leichtgewichtige, von OS-Threads entkoppelte Threads, die während einer blockierenden Operation der JDK (I/O, Sperren, sleep und Ähnliches in der Standardbibliothek) ihren OS-Thread freigeben, sodass sich mehrere Millionen von ihnen in einer einzigen JVM ausführen lassen. Allerdings gilt das nicht für „jede Art von Blockierung“. Blockiert ein virtueller Thread während der Ausführung von nativem Code (JNI) oder einer Foreign Function, bleibt er an seinen Carrier-Thread gepinnt. Was JDK 24 (das unten besprochene JEP 491) behoben hat, ist das Pinning durch synchronized; das Pinning an nativen Grenzen bleibt bestehen, sodass ein Vorgang, der über einen JNI-Treiber oder eine Geräte-API lange blockiert, bei Verwendung vieler virtueller Threads die Carrier-Threads erschöpft. Wie der offizielle Leitfaden aber betont, sind es keine „schnelleren Threads“. Die Ausführungsgeschwindigkeit des Codes ändert sich nicht – was sie bieten, ist Skalierung (Durchsatz).3

Es gibt drei Regeln der Disziplin für ihre Verwendung.3

  1. Nicht poolen. Virtuelle Threads sind billig und zum Wegwerfen bestimmt; „Anzahl der Aufgaben = Anzahl der virtuellen Threads“ ist der korrekte Zustand. Virtuelle Threads in einen newFixedThreadPool zu stecken, ist ein Fehler – verwenden Sie die Form try (var executor = Executors.newVirtualThreadPerTaskExecutor()).
  2. Begrenzen Sie die Nebenläufigkeit mit Semaphore. Drücken Sie eine Beschränkung wie „höchstens 10 gleichzeitige Verbindungen zu einer externen API“ über ein Semaphor aus, nicht über die Poolgröße.
  3. Nicht für CPU-gebundene Arbeit verwenden. Für die Parallelisierung von Berechnungen bleiben wie bisher Plattform-Threads in Kernzahl-Größenordnung die richtige Wahl.

Beachten Sie, dass innerhalb eines virtuellen Threads ganz gewöhnlicher synchroner Code läuft. Statt den Code wie beim async/await von .NET umzuschreiben, besteht die Entwurfsphilosophie virtueller Threads darin, „einfachen Code nach dem Muster ein Request, ein Thread“ unverändert in großer Zahl laufen zu lassen.2

3.3. Ein häufiges Missverständnis — „kein Pool nötig“ gilt nur für virtuelle Threads

Lesen Sie aus der Regel „nicht poolen“ bitte nicht heraus, dass „Java keinen Thread-Pool-Mechanismus besitzt (oder er ineffizient wäre)“. Tatsächlich ist das Gegenteil der Fall: Javas Pools sind seit JDK 5 (2004) ein ausgereifter Bestandteil der Standardbibliothek. Der fein konfigurierbare Allzweckpool ThreadPoolExecutor (erstellt über die verschiedenen Executors-Factorys), der Work-Stealing-Pool ForkJoinPool (dessen gemeinsame Instanz commonPool() das Standardziel für parallel stream und CompletableFuture ist) sowie ScheduledThreadPoolExecutor für periodische Ausführung – bei CPU-gebundener Arbeit spielen diese nach wie vor die Hauptrolle.

Ein Pool ist im Grunde eine Optimierung nach dem Prinzip „Erstellung und Vorhaltung eines OS-Threads sind teuer, also wiederverwenden“. Virtuelle Threads beseitigen diese Voraussetzung, indem sie die Erstellungskosten praktisch auf null senken, sodass es keinen Grund mehr zur Wiederverwendung gibt — die zutreffende Sichtweise ist nicht, dass Pooling ineffizient geworden wäre, sondern dass Threads so leichtgewichtig geworden sind, dass die Optimierung durch Pooling überflüssig ist. Und unter der Oberfläche der virtuellen Threads betreibt der Scheduler der JDK als Work-Stealing-ForkJoinPool eine Menge von Carrier-Threads (OS-Threads) in Kernzahl-Größenordnung.2 Das Grundmuster „eine kleine Anzahl von OS-Threads bewältigt eine riesige Menge an Nebenläufigkeit“ bleibt also erhalten – nur die Verwaltung dieses Pools ist aus der Hand des Entwicklers an die JVM übergegangen. Man kann sagen, dass Java auf demselben Zielpunkt landet wie das async/await von .NET, das seinen Thread zum Zeitpunkt des await an den Pool zurückgibt – nur ohne dass sich die Form des Codes ändert.

4. Gemeinsam genutzten veränderlichen Zustand reduzieren — Aufteilen, Unveränderlichkeit, nebenläufige Collections und Warteschlangen

Ein Konflikt entsteht nur, wenn „mehrere Threads“ und „gemeinsam genutzte veränderliche Daten“ zusammenkommen. Die Anzahl der Threads wird durch die Anforderungen bestimmt, sodass sich im Entwurf nur die gemeinsame Nutzung reduzieren lässt. Es gibt drei Mittelfamilien – „Aufteilen“, „Unveränderlichmachen“ und „Weiterreichen“ –, die sich in Java so schreiben lassen.

Aufteilen. Lassen Sie bei der parallelen Aggregation nicht jeden Thread in eine gemeinsame Summenvariable schreiben, sondern jeden Thread ein Teilergebnis bilden und diese am Ende zusammenführen. reduce / collect von parallel stream bieten genau diese Form als Rahmenwerk, und auch das weiter unten besprochene LongAdder ist eine Implementierung dieser Aufteilungsstrategie: Intern werden Zellen aufgeteilt, um Konflikte zu streuen, und beim Lesen zusammengerechnet. Die Häufigkeit der Schreibzugriffe auf gemeinsamen Zustand zu reduzieren, kommt vor dem korrekten Schreiben von Synchronisierung.

Unveränderlich machen. Bilden Sie mit record und unveränderlichen Collections (List.copyOf / Map.copyOf) Daten, die nach der Konstruktion nicht mehr überschrieben werden, lassen sich diese ohne Synchronisierung teilen. Für Konfiguration und Stammdaten ist die Standardform: „beim Ersetzen ein neues Objekt bauen und eine volatile-Referenz umhängen“. Allerdings sind „sieht schreibgeschützt aus“ und „ist unveränderlich“ zwei verschiedene Dinge. Die Zugriffsmethoden eines record geben die Referenzen seiner Komponenten unverändert zurück, und auch die Kopie von List.copyOf ist flach (sie dupliziert die Elementobjekte nicht) – sind die Elemente veränderlich, kann jeder, der einen Alias besitzt, den Inhalt überschreiben, und der Konflikt bleibt bestehen. Ohne Synchronisierung teilen dürfen Sie nur dann, wenn der gesamte Objektgraph einschließlich der Elemente unveränderlich ist. Enthält er veränderliche Elemente, übergeben Sie entweder eine tiefe Kopie, oder machen Sie auch die Elemente zu record- bzw. unveränderlichen Typen.

Die zusammengesetzten Operationen nebenläufiger Collections nutzen. Für das „falls nicht vorhanden, anlegen und einfügen“ von ConcurrentHashMap verwenden Sie computeIfAbsent. Diese Methode wird als Ganzes atomar ausgeführt, und ist der Schlüssel nicht vorhanden, wird die Mapping-Funktion innerhalb dieses einen Aufrufs genau einmal aufgerufen.8 Dass sich die Garantie von .NETs ConcurrentDictionary.GetOrAdd unterscheidet (dessen Factory bei Konkurrenz mehrfach laufen kann), ist ein Punkt, den Menschen, die zwischen beiden Sprachen wechseln, leicht verwechseln. Es gilt jedoch nicht „genau einmal über die gesamte Lebensdauer des Schlüssels“. Gibt die Funktion null zurück oder wirft sie eine Ausnahme, wird keine Zuordnung registriert, und die Funktion wird beim nächsten Aufruf erneut ausgeführt (dasselbe gilt, wenn der Eintrag nach der Registrierung entfernt wird). Entwerfen Sie eine Initialisierung, die keine doppelten Seiteneffekte verträgt, so, dass die Funktion nicht-null zurückgibt und erfolgreich abschließt. Als Preis der Atomarität werden während der Berechnung jedoch einige Aktualisierungen anderer Threads blockiert, weshalb Sie die Mapping-Funktion kurz halten und diese Map innerhalb der Funktion nicht selbst aktualisieren dürfen (eine erkannte rekursive Aktualisierung kann zu IllegalStateException führen).8

// Der Standardansatz für einen Häufigkeitszähler: computeIfAbsent + LongAdder
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();

Über eine Warteschlange weiterreichen. Leiten Sie den Datenfluss zwischen Threads über BlockingQueue. Bei einer ArrayBlockingQueue mit angegebener Kapazität blockiert put, wenn sie voll ist, was zu natürlicher Backpressure führt – dasselbe Bild wie beim bounded Channel aus der .NET-Edition. Auch im Zeitalter der virtuellen Threads bleibt dieser Entwurf, der die Grenze zwischen Producer und Consumer klar zieht, wirksam.

5. Sperrdisziplin — synchronized und ReentrantLock

5.1. Worauf man sperrt und was man während des Sperrens unterlässt

Denken Sie bei der Einheit der Sperre nicht in „Codeabschnitten“, sondern in „Daten“. Ordnen Sie jeder Menge zu schützender veränderlicher Daten genau ein Sperrobjekt zu, und nehmen Sie an allen Stellen, die diese Daten berühren, dieselbe Sperre — ein zusammenbrechendes Zuordnungsschema genau dieser Art ist in der Praxis die Ursache von Race-Bugs. Vermeiden Sie synchronized(this) und synchronized(SomeClass.class), da externer Code dasselbe Objekt sperren kann, und ordnen Sie stattdessen ein nicht nach außen sichtbares private final Object lock = new Object(); eins-zu-eins den zu schützenden Daten zu.

Dazu zwei weitere Disziplinregeln. Erstens: Tun Sie während des Sperrens nichts Zeitaufwendiges oder Externes. I/O, das Aufrufen von Listenern oder das Ausführen unbekannten Codes bei gehaltener Sperre verlängert nicht nur die Haltezeit, sondern schafft auch einen Pfad, auf dem der aufgerufene Code versucht, eine andere Sperre zu nehmen, und so die zirkuläre Wartesituation aus Abbildung 2 erzeugt. Zweitens: Legen Sie die Erwerbsreihenfolge mehrerer Sperren fest. Machen Sie an Stellen, an denen zwei oder mehr Sperren genommen werden, zur Regel, dass alle Threads dieselbe Reihenfolge einhalten, und richten Sie dort, wo Sie die Reihenfolge nicht garantieren können, mit dem unten beschriebenen tryLock(timeout) einen Pfad „falls nicht erlangbar, aufgeben und erneut versuchen“ ein.

synchronized genügt für „kurzen, einfachen Ausschluss“. Wechseln Sie zu ReentrantLock, sobald Sie Folgendes benötigen.

  • Zeitlich begrenzte Erlangung über tryLock(timeout) (verwandelt einen endlosen Hang in einen Fehler, den Sie protokollieren und behandeln können)
  • Fairness-Policy, mehrere Condition-Objekte, oder wenn Sie Erlangen und Freigeben der Sperre auf verschiedene Methoden aufteilen möchten

Verwenden Sie ReentrantLock, brechen Sie niemals das Muster auf, direkt nach lock() ein try zu setzen und unlock() in einem finally-Block aufzurufen (Java hat keine dem RAII von C++ entsprechende Syntax, sodass dieses Muster die gesamte Disziplin ausmacht).

5.2. Virtuelle Threads und Pinning — was sich mit JDK 24 geändert hat

Bei der ursprünglichen Einführung virtueller Threads (JDK 21–23) gab es die Einschränkung, dass eine Blockierung innerhalb eines synchronized-Blocks den virtuellen Thread an seinen OS-Thread pinnt (er kann den OS-Thread nicht freigeben, wodurch der Skalierungsvorteil verloren geht), weshalb bei häufigen oder lang andauernden Blockierungen der Umstieg auf ReentrantLock empfohlen wurde.3 Diese Einschränkung wurde behoben, als JEP 491 in JDK 24 die Monitor-Implementierung neu schrieb, und synchronized pinnt virtuelle Threads seither nicht mehr.4 Verwenden Sie JDK 24 oder neuer, ist ein mechanischer Austausch als Gegenmaßnahme gegen Pinning nicht mehr nötig. Es lohnt sich zu prüfen, ob interne Richtlinien noch auf dem Warnstand von JDK 21 stehen geblieben sind.

5.3. Wo Atomics und volatile hingehören

Atomare Aktualisierungen einer einzelnen Variable übernehmen AtomicInteger / AtomicLong / AtomicReference (bei Statistikwerten, die nur mit hoher Frequenz erhöht werden, das gegen Konkurrenz robuste LongAdder). volatile garantiert Sichtbarkeit und Ordnung (happens-before), nicht die Atomarität zusammengesetzter Operationen.5 Dieselbe Schlussfolgerung wie in der .NET- und der C++-Edition gilt auch für Java: Flags und einzelne Werte gehören zu Atomics, zusammengesetzter Zustand zu Sperren, und volatile sollte es nicht allein richten müssen.

6. Entwurf des Anhaltens — Interrupts als gemeinsame Sprache

6.1. Die Etikette von interrupt

Das Stoppen und Abbrechen in Java ist einheitlich auf Interrupts ausgerichtet. t.interrupt() setzt den Interrupt-Status des Ziel-Threads, und blockiert das Ziel gerade in sleep / wait / join oder Ähnlichem, wirft es InterruptedException, um es sofort zu wecken (dabei wird der Interrupt-Status zurückgesetzt).6 Die einstigen erzwingenden Mittel Thread.stop / suspend / resume sind grundsätzlich unsicher, weshalb ein Aufruf heute UnsupportedOperationException auslöst.6

Kann selbst beendenKann nicht beenden (z. B. innerhalb einer Bibliothek)Die stoppende Seite: t.interrupt()Interrupt-Status wird gesetztThread mitten in der Berechnung:prüft Thread.interrupted() in einer SchleifeBlockiert in sleep / wait / join:InterruptedException fliegt und weckt sofort(Status wird zurückgesetzt)Räumt auf und endet selbstWas tut der catch-Block?Thread.currentThread().interrupt()stellt den Status wieder her und hinterlässt das Signal

Abbildung 4: Kooperative Abbruchsteuerung über Interrupts. Wird InterruptedException verschluckt, verschwindet das Stopp-Signal — nach dem Fangen bleibt nur die Wahl zwischen “beenden” und “wiederherstellen”

In der Praxis genügt eine einzige Regel: Schreiben Sie keinen Code, der InterruptedException fängt und nichts tut. Können Sie im Rahmen Ihrer eigenen Verantwortung beenden, tun Sie das dort; können Sie es nicht, stellen Sie mit Thread.currentThread().interrupt() den Status wieder her und geben das Signal an den Aufrufer weiter (siehe FAQ).

6.2. Das zweistufige Herunterfahren von ExecutorService

Die Stopp-APIs von ExecutorService bauen auf dem Interrupt-Modell auf. shutdown() stoppt die Annahme neuer Aufgaben und lässt bereits eingereichte Aufgaben vollständig durchlaufen, shutdownNow() versucht, laufende Aufgaben zu stoppen. Als Schnittstellenspezifikation ist das Best-Effort, und es ist ausdrücklich festgehalten, dass die Standardimplementierung (etwa ThreadPoolExecutor) typischerweise über Thread.interrupt() abbricht — das heißt, Aufgaben, die nicht auf Interrupts reagieren, stoppen auch mit shutdownNow nicht, und verwenden Sie eine eigene Executor-Implementierung, müssen Sie deren Abbruchmethode (ob überhaupt ein Interrupt gesendet wird) in der Dokumentation der Implementierung nachschlagen.1 Das von der offiziellen Dokumentation gezeigte Standardmuster für das Stoppen ist das folgende zweistufige Muster.1

/** true, sobald das Herunterfahren abgeschlossen ist. Bei false nicht mit der Freigabe gemeinsamer Ressourcen fortfahren. */
boolean shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown();                    // Stufe 1: Annahme neuer Aufgaben stoppen und auf den Abschluss warten
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            // Stufe 2: Abbruch anfordern. Aufgaben, die vor der Ausführung verworfen wurden,
            // werden zurückgegeben - deren Futures als abgebrochen markieren, um auf get() wartende Aufrufer zu wecken
            pool.shutdownNow().forEach(r -> {
                if (r instanceof Future<?> f) f.cancel(false);
            });
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Pool did not terminate");
                return false;           // Herunterfahren nicht abgeschlossen. Von Erfolg unterscheidbar melden
            }
        }
        return true;
    } catch (InterruptedException ex) {
        pool.shutdownNow().forEach(r -> {
            if (r instanceof Future<?> f) f.cancel(false);
        });
        Thread.currentThread().interrupt();   // Auch den eigenen Interrupt-Status wiederherstellen
        return false;                   // Auch auf diesem Pfad ist das Herunterfahren womöglich unvollständig
    }
}
innerhalb der Frist abgeschlossenZeitüberschreitungabgeschlossennoch nicht beendetshutdown()stoppt die Annahme neuer AufgabenawaitTerminationwartet auf den AbschlussHerunterfahren abgeschlossenshutdownNow()sendet Interrupt an laufende Aufgaben(ob sie reagieren, hängt von der Aufgabe ab)awaitTerminationwartet erneutals Anomalie protokollieren(verdächtig sind Aufgaben, die nicht auf Interrupts reagieren)

Abbildung 5: Das zweistufige Herunterfahren von ExecutorService. Ein gestuftes Vorgehen nach dem Muster “höflich warten → über Interrupt anfordern → wenn immer noch nicht beendet, als Anomalie beobachten”

Beachten Sie außerdem eine Grenze beim Abbrechen der von shutdownNow() zurückgegebenen Runnable-Objekte. Zurückgegeben wird das Objekt, das in der Ausführungswarteschlange stand – bei einem einfachen submit ist das genau der FutureTask, der dem Aufrufer übergeben wurde, aber bei Aufgaben, die über einen Wrapper wie ExecutorCompletionService eingereicht wurden, ist es der Wrapper in der Warteschlange, ein anderes Objekt als das Future des Aufrufers. In diesem Aufbau bringt der obige Abbruch das Future des Aufrufers nicht zum Abschluss. Entwerfen Sie das System daher so, dass Sie bei der Einreichung selbst eine Liste der Future-Objekte führen und diese beim Herunterfahren abbrechen (oder die verworfenen Aufgaben an ihren Besitzer zurückgeben).

close() (AutoCloseable), verfügbar ab JDK 19, verpackt „shutdown aufrufen und auf den Abschluss warten“ in eine Form, die sich mit try-with-resources schreiben lässt, und try (var executor = ...) in Kombination mit dem virtuellen newVirtualThreadPerTaskExecutor ist die heute übliche Grundform.1 close() ist jedoch kein Ersatz für das obige zweistufige Muster. Da es ohne Timeout auf den Abschluss wartet, blockiert der Thread, der zu schließen versucht, für immer, sobald auch nur eine Aufgabe nicht auf Interrupts reagiert oder nie endet. Es ist ein Werkzeug für Situationen, in denen die Aufgaben im Geltungsbereich endlich sind und ihr vollständiger Abschluss garantiert ist (an Ort und Stelle einreichen und an Ort und Stelle warten); verwenden Sie für Stellen wie den Shutdown-Pfad einer Anwendung, an denen Sie „unbedingt in endlicher Zeit fertig werden“ müssen, das befristete zweistufige Muster. Das Abbrechen einzelner Aufgaben erfolgt ebenfalls über Interrupts, mit Future.cancel(true).

7. Der UI-Thread — Swings EDT

Für Desktopanwendungen gilt unabhängig von Sprache oder Framework das Gesetz, dass die UI ausschließlich dem sie verwaltenden Thread gehört. In Swing ist dieser exklusive Thread der Event Dispatch Thread (EDT): Die Methoden von Swing-Komponenten sind grundsätzlich nicht threadsicher, und der Zugriff von mehreren Threads aus zieht Thread-Interferenzen und Speicherkonsistenzfehler nach sich. Aktualisierungen der Anzeige von einem anderen Thread aus werden über SwingUtilities.invokeLater beim EDT beauftragt, und da umgekehrt lange Verarbeitungen auf dem EDT die UI einfrieren lassen, lagern Sie aufwendige Arbeit über SwingWorker oder Ähnliches an einen Worker-Thread aus.7 Bei JavaFX ist die Struktur dieselbe: UI-Aktualisierungen werden über Platform.runLater beim Anwendungs-Thread beauftragt.

8. Verifikation und Debugging — Thread-Dumps als Waffe

Sie können nicht erwarten, dass Race-Bugs durch Tests gefunden werden. Gewöhnliche Tests zählen einen Lauf, bei dem „zufällig kein Konflikt aufgetreten ist“, als Erfolg. Denken Sie Ihre Vorbereitung in drei Schichten.

Die erste Verteidigungslinie ist der Entwurf. Prüfen Sie im Review tabellarisch: „Welche veränderlichen Daten werden geteilt?“, „Welche Sperre schützt jeweils welche (die Zuordnung aus 5.1)?“, „Ist die Erwerbsreihenfolge der Sperren eindeutig?“, „Gibt es einen catch-Block, der InterruptedException verschluckt?“ und „Erreicht der Stopp-Pfad (shutdown/Interrupt) alle Aufgaben?“

Zweitens setzen Sie Thread-Dumps gekonnt ein. Java besitzt ein Standardwerkzeug, um „den Zustand aller Threads in diesem eingefrorenen Moment“ zu erfassen: jstack (oder jcmd <pid> Thread.print) gibt Stacktraces aus, und die Option -l liefert zusätzlich Informationen zu Sperren.9 Beachten Sie, dass dieses klassische Dump-Format für Plattform-Threads gedacht ist und die virtuellen Threads Ihrer Anwendung nicht enthält. Verfolgen Sie eine blockierte Anfrage in einem Aufbau, der virtuelle Threads verwendet (Kapitel 3), nutzen Sie jcmd <pid> Thread.dump_to_file -format=json <Datei>, das auch virtuelle Threads mit erfasst.2 Bei der Untersuchung eines Hangs ist es das übliche Vorgehen, alle paar Sekunden zwei bis drei Dumps zu erfassen und abzugleichen, auf welche Sperre die untätigen Threads jeweils warten und wer diese Sperre hält. Protokollieren Sie Zeitüberschreitungen von tryLock(timeout) (5.2), lässt sich sogar der Anlass zum Erfassen eines Dumps automatisieren.

Drittens rütteln Sie unter Last daran. Stresstests – mit mehr Parallelität als Kernen über längere Zeit laufen lassen, die Verarbeitungsreihenfolge randomisieren, künstliche Verzögerungen einfügen – sind ein realistisches Mittel, um es auf der Entwicklungsmaschine wahrscheinlicher zu machen, einen Konflikt zu „treffen“. Führen Sie vor der Veröffentlichung mindestens einmal einen Test mit produktionsnaher Datenmenge und Thread-Anzahl durch.

9. Die Zukunft der Java-Nebenläufigkeit — strukturierte Nebenläufigkeit

Zum Abschluss noch ein kurzer Blick nach vorn. Auf der Grundlage virtueller Threads befindet sich Structured Concurrency (StructuredTaskScope) in der Entwicklung – ein Konzept, das „mehrere Teilaufgaben als eine Arbeitseinheit behandelt und die Weitergabe von Fehlern sowie den Abbruch strukturiert“ –, das mit Stand August 2026 noch ein Preview-Feature ist. In der fünften Preview von JDK 25 (JEP 505) wurde es zu einer API-Form über StructuredTaskScope.open() umgestaltet und setzt sich im aktuellen JDK 26 als sechste Preview (JEP 525) fort.1011 Die unveränderliche Kontextfreigabe Scoped Values, die die Probleme von ThreadLocal löst, wurde dagegen in JDK 25 finalisiert.12 Die Prinzipien dieses Artikels – klare Aufgabengrenzen, unveränderliche gemeinsame Nutzung, kooperatives Stoppen – decken sich auch mit der Richtung, in die diese neuen APIs zielen.

10. Zusammenfassung — die Checkliste für Java

  1. Ist im Geschäftscode noch new Thread übrig geblieben (oder setzt er auf ExecutorService / virtuelle Threads)?
  2. Werden I/O-gebundene und CPU-gebundene Arbeit unterschiedlichen Ausführungsmitteln zugewiesen (die Verzweigung aus Abbildung 3)?
  3. Werden virtuelle Threads nicht gepoolt, und wird die Begrenzung der Nebenläufigkeit über Semaphore ausgedrückt?
  4. Sind gemeinsam genutzte Daten unveränderlich (record / List.copyOf) oder auf den Werkzeugen von java.util.concurrent aufgebaut?
  5. Gibt es kein synchronized(this) und keine Sperre auf ein öffentlich sichtbares Objekt?
  6. Werden die zusammengesetzten Operationen von ConcurrentHashMap (computeIfAbsent usw.) verwendet und die Mapping-Funktion kurz gehalten?
  7. Wird von volatile keine Atomarität erwartet (verwenden Zähler die Atomic-Klassen / LongAdder)?
  8. Gibt es keinen einzigen catch-Block, der InterruptedException verschluckt?
  9. Folgt das Stoppen von ExecutorService dem befristeten zweistufigen Muster (und ist die Verwendung von close() auf Bereiche beschränkt, in denen der vollständige Abschluss der Aufgaben garantiert ist)?
  10. Sind die UI-Aktualisierungen von Swing/JavaFX auf dem EDT bzw. dem Anwendungs-Thread gebündelt?

Java ist eine der Sprachen mit dem am besten ausgestatteten Werkzeugkasten für Nebenläufigkeit, und mit dem Aufkommen der virtuellen Threads hat sich auch ein Weg eröffnet, einfachen synchronen Code unverändert zu skalieren. Gerade deshalb ist das korrekte Verständnis der Arbeitsteilung der Werkzeuge — welches für Durchsatz zuständig ist, welches für Ausschluss, und was das Signal zum Stoppen ist — der eigentliche Kern des Multithreading-Designs in Java.

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt Multithreading-Design-Reviews für Geschäftssysteme und Batch-Verarbeitung in Java, die Ursachenuntersuchung nebenläufigkeitsbedingter Fehler wie beschädigten gemeinsamen Zustands oder „stoppt gelegentlich nicht / friert ein“ (Thread-Dump-Analyse) sowie technische Beratung zur Einführung virtueller Threads.

  1. Oracle, ExecutorService (Java SE 21 & JDK 21 API). Dazu, dass shutdown() bereits eingereichte Aufgaben vollständig durchlaufen lässt und gleichzeitig die Annahme neuer Aufgaben stoppt; dazu, dass shutdownNow() versucht, laufende Aufgaben zu stoppen, und eine Liste wartender Aufgaben zurückgibt, wobei die typische Implementierung über Thread.interrupt() abbricht und keine über Best-Effort hinausgehende Garantie bietet, sodass eine nicht auf Interrupts reagierende Aufgabe nicht endet; dazu, dass sich mit awaitTermination auf den Abschluss warten lässt; dazu, dass close() (AutoCloseable, ab Java 19) shutdown aufruft und auf den Abschluss wartet und sich mit try-with-resources verwenden lässt; sowie dazu, dass das zweistufige Herunterfahren shutdown → awaitTermination → shutdownNow als Anwendungsbeispiel gezeigt wird.  2 3 4 5 6

  2. OpenJDK, JEP 444: Virtual Threads. Dazu, dass virtuelle Threads in JDK 21 zu einem offiziellen Feature wurden; dazu, dass es sich um leichtgewichtige Threads handelt, die den Aufwand für das Schreiben, Warten und Beobachten hochdurchsatzstarker nebenläufiger Anwendungen erheblich senken; zur Entwurfsphilosophie, einfachen synchronen Code nach dem Muster „ein Request, ein Thread“ unverändert zu skalieren; dazu, dass der Scheduler der JDK für virtuelle Threads ein im FIFO-Modus arbeitender Work-Stealing-ForkJoinPool ist, dessen Standardparallelität der Anzahl verfügbarer Prozessoren entspricht; sowie dazu, dass ein neues Thread-Dump-Format, das virtuelle Threads einschließt, als jcmd Thread.dump_to_file (im Klartext- und im JSON-Format) hinzugefügt wurde, während klassische Thread-Dumps keine virtuellen Threads enthalten.  2 3 4 5

  3. Oracle Java SE Core Libraries, Virtual Threads. Dazu, dass virtuelle Threads leichtgewichtige, von der Java-Laufzeit implementierte Threads sind, die bei blockierendem I/O ihren OS-Thread freigeben; dazu, dass es sich um ein Feature für Skalierung (Durchsatz) statt Geschwindigkeit (Latenz) handelt, das für CPU-intensive Verarbeitung ungeeignet ist; dazu, dass virtuelle Threads niemals gepoolt, sondern pro Aufgabe einzeln verwendet werden sollten (newVirtualThreadPerTaskExecutor); dazu, dass für die Begrenzung der Nebenläufigkeit Semaphore statt eines Thread-Pools verwendet werden sollte; dazu, dass eine Blockierung innerhalb von synchronized zum Stand von JDK 21 ein Pinning an den OS-Thread verursacht, weshalb bei häufigen oder lang andauernden Fällen der Umstieg auf ReentrantLock empfohlen wurde; sowie dazu, dass sich Pinning mit -Djdk.tracePinnedThreads erkennen lässt.  2 3 4 5 6 7

  4. OpenJDK, JEP 491: Synchronize Virtual Threads without Pinning. Dazu, dass die Monitor-Implementierung der JVM in JDK 24 für die Unterstützung virtueller Threads neu geschrieben wurde, sodass eine Blockierung innerhalb eines synchronized-Blocks oder einer synchronized-Methode virtuelle Threads nicht mehr an ihren Carrier-Thread pinnt; sowie dazu, dass die aus der JDK-21-23-Ära stammende Gegenmaßnahme, synchronized durch ReentrantLock zu ersetzen, dadurch grundsätzlich überflüssig wird.  2

  5. Oracle, The Java Tutorials, Memory Consistency Errors. Dazu, dass Speicherkonsistenzfehler entstehen, wenn mehrere Threads eine inkonsistente Sicht auf dieselben Daten haben; dazu, dass der Schlüssel zur Vermeidung die happens-before-Beziehung ist (die Garantie, dass ein Speicherschreibvorgang einer Anweisung für eine andere Anweisung sichtbar ist); sowie dazu, dass synchronized, volatile sowie Thread.start / join unter anderem happens-before-Beziehungen erzeugen.  2 3

  6. Oracle, Thread (Java SE 21 & JDK 21 API). Dazu, dass Thread.stop / suspend / resume grundsätzlich unsicher sind (Sperren werden in einem inkonsistenten Zustand freigegeben, wodurch beschädigte Objekte sichtbar werden; suspend kann zu Deadlocks führen) und daher zur Entfernung vorgesehen als veraltet gelten, wobei ein Aufruf heute UnsupportedOperationException auslöst; dazu, dass interrupt() den Interrupt-Status setzt und einen in sleep / wait / join blockierten Thread durch Werfen von InterruptedException weckt (wobei der Interrupt-Status dabei zurückgesetzt wird); sowie zum Unterschied, wie interrupted() und isInterrupted() mit diesem Status umgehen.  2 3

  7. Oracle, The Java Tutorials, The Event Dispatch Thread. Dazu, dass der Ereignisverarbeitungscode von Swing auf dem Event Dispatch Thread (EDT) läuft; dazu, dass die Methoden der meisten Swing-Objekte nicht threadsicher sind und Aufrufe von mehreren Threads aus Thread-Interferenzen und Speicherkonsistenzfehler nach sich ziehen, weshalb der Zugriff auf Swing-Komponenten grundsätzlich auf dem EDT erfolgen sollte; dazu, dass von anderen Threads aus Aufgaben über SwingUtilities.invokeLater / invokeAndWait beim EDT beauftragt werden; sowie dazu, dass Aufgaben auf dem EDT kurz gehalten werden sollten.  2

  8. Oracle, ConcurrentHashMap (Java SE 21 & JDK 21 API). Dazu, dass der gesamte Methodenaufruf von computeIfAbsent atomar ausgeführt wird und die Mapping-Funktion bei fehlendem Schlüssel genau einmal aufgerufen wird; dazu, dass während der Berechnung ein Teil der Aktualisierungsoperationen anderer Threads blockiert wird, weshalb die Berechnung kurz und einfach gehalten werden sollte; dazu, dass die Mapping-Funktion diese Map nicht selbst verändern darf und eine erkannte rekursive Aktualisierung zu IllegalStateException führt; sowie dazu, dass eine Abrufoperation (get) nicht blockiert und zwischen einer Aktualisierung für einen bestimmten Schlüssel und einem nachfolgenden Abruf eine happens-before-Beziehung besteht.  2 3

  9. Oracle, The jstack Command (Java SE 21 Tools Reference). Dazu, dass jstack die Stacktraces (Klassenname, Methodenname, Zeilennummer) aller Threads eines angegebenen Java-Prozesses ausgibt; dazu, dass sich mit der Option -l eine Detailansicht mit zusätzlichen Informationen zu Sperren erstellen lässt; sowie dazu, dass es zusammen mit anderen Diagnosewerkzeugen wie jcmd verwendet wird. 

  10. OpenJDK, JEP 505: Structured Concurrency (Fifth Preview). Dazu, dass die API für strukturierte Nebenläufigkeit eine Gruppe zusammengehöriger Teilaufgaben als eine Arbeitseinheit behandelt und Fehlerweitergabe sowie Abbruch strukturiert; dazu, dass StructuredTaskScope zu einer Form umgestaltet wurde, die über eine statische Factory-Methode (open) geöffnet wird; sowie dazu, dass es sich zum Stand von JDK 25 um die fünfte Preview handelt und nicht um ein finalisiertes Feature. 

  11. OpenJDK, JEP 525: Structured Concurrency (Sixth Preview). Dazu, dass strukturierte Nebenläufigkeit auch in JDK 26 als sechste Preview fortgesetzt wird — das heißt, mit Stand August 2026 im aktuellen JDK weiterhin ein Preview-Feature ist, dessen Nutzung die Aktivierung von Preview-Features erfordert. 

  12. OpenJDK, JEP 506: Scoped Values. Dazu, dass Scoped Values in JDK 25 finalisiert wurden; sowie dazu, dass es sich um einen Mechanismus handelt, um unveränderliche Kontextdaten innerhalb und zwischen Threads sicher und effizient zu teilen, der eine Lösung für die Probleme von ThreadLocal (Veränderlichkeit, Lebenszyklusverwaltung, Vererbungskosten) darstellt. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Brauche ich mit virtuellen Threads überhaupt noch einen Thread-Pool (ExecutorService)?
Das hängt vom Einsatzzweck ab. Virtuelle Threads sind ein Mechanismus, um riesige Mengen überwiegend auf I/O wartender Aufgaben laufen zu lassen – sie machen Code nicht schneller, sondern erhöhen den Durchsatz. Verwenden Sie für I/O-gebundene Arbeit pro Aufgabe einen virtuellen Thread (Executors.newVirtualThreadPerTaskExecutor), und poolen Sie virtuelle Threads niemals. Für die Parallelisierung von Berechnungen, die die CPU voll auslasten, eignet sich dagegen wie bisher ein auf etwa die Kernzahl begrenzter Pool aus Plattform-Threads (oder ein parallel stream). Wenn Sie außerdem die Anzahl gleichzeitiger Zugriffe auf einen externen Dienst begrenzen möchten, empfiehlt sich im Zeitalter der virtuellen Threads eine Begrenzung über Semaphore statt über die Poolgröße.
Sollte ich synchronized oder ReentrantLock verwenden?
Für kurzen, einfachen Ausschluss reicht synchronized völlig aus, und der Code bleibt knapp. Greifen Sie zu ReentrantLock, wenn Sie Funktionen wie eine zeitlich begrenzte Erlangung über tryLock, eine Fairness-Policy oder mehrere Conditions benötigen. In Kombination mit virtuellen Threads gibt es einen historischen Vorbehalt: In JDK 21–23 gab es das Problem, dass eine Blockierung innerhalb eines synchronized-Blocks den virtuellen Thread an seinen OS-Thread pinnte, weshalb bei häufigen oder lang andauernden Blockierungen der Umstieg auf ReentrantLock empfohlen wurde. In JDK 24 (JEP 491) wurde die Monitor-Implementierung neu geschrieben, wodurch diese Einschränkung entfällt. Ab JDK 24 ist ein Austausch allein wegen des Pinnings nicht mehr nötig.
Macht das Hinzufügen von volatile etwas threadsicher?
Nein. Javas volatile erzeugt eine happens-before-Beziehung zwischen dem Schreiben und dem Lesen dieser Variablen und garantiert damit Sichtbarkeit (dass andere Threads den neuesten Schreibvorgang sehen) und Ordnung, nicht aber die Atomarität einer zusammengesetzten Operation wie „lesen, berechnen, zurückschreiben“. Führen mehrere Threads ++ auf einem volatile int-Zähler aus, gehen Additionen verloren. Verwenden Sie für Zähler AtomicInteger / AtomicLong (bei hochfrequenter Aggregation LongAdder) und für das gemeinsame Schützen mehrerer Variablen eine Sperre. volatile eignet sich praktisch nur für den Fall eines einfachen Zustands-Flags, bei dem ein Thread schreibt und die anderen nur lesen.
Darf ich InterruptedException fangen und ignorieren?
Nein. Interrupts sind Javas Standardsignal für Stoppen und Abbrechen, und sie zu verschlucken erzeugt einen „Thread, der sich nicht stoppen lässt“. Zum Zeitpunkt, an dem InterruptedException geworfen wird, ist der Interrupt-Status bereits zurückgesetzt. Können Sie die Arbeit nicht selbst beenden, stellen Sie den Status mit Thread.currentThread().interrupt() wieder her, um das Signal für den Aufrufer zu hinterlassen, oder werfen Sie die Ausnahme unverändert weiter. Ein leerer catch-Block, der nichts tut, ist eine klassische Ursache für Fehler wie ein wirkungsloses Shutdown oder ein ignoriertes shutdownNow.
Kann ich einen Thread nicht mit Thread.stop anhalten?
Nein, das geht nicht. Thread.stop ist grundsätzlich unsicher (Sperren werden in einem inkonsistenten Zustand freigegeben, sodass beschädigte Objekte für andere Threads sichtbar werden) und gilt daher schon lange als veraltet; im heutigen Java löst ein Aufruf UnsupportedOperationException aus. Dasselbe gilt für Thread.suspend / resume. Das einzig legitime Mittel, einen Thread zu stoppen, ist die kooperative Abbruchsteuerung über interrupt. Verwenden Sie ExecutorService, stoppt shutdown lediglich die Annahme neuer Aufgaben und wartet auf deren Abschluss, ohne laufenden Aufgaben ein Interrupt zu senden. Erst shutdownNow versucht, laufende Aufgaben zu stoppen, und das ist Best-Effort (in der Standardimplementierung über Interrupts).

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog