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

· Aktualisiert am: · · Multithreading, Java, Geschäftsanwendungen, Fehleruntersuchung, Design

Änderungsverlauf (Erstfassung, veröffentlicht am 2. Aug 2026)
Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22175922)

Die folgenden DOIs verweisen auf bereits archivierte Versionen, die vom aktuellen Text abweichen können. Verwenden Sie die URL dieser Seite, um auf den aktuellen Text zu verweisen.

Go Komura (2026). Praktische Best Practices für Multithreading: Java-Edition — Konventionen im Zeitalter der virtuellen Threads. KomuraSoft LLC. https://comcomponent.com/de/blog/multithreading-best-practices-java/

DOI (registriertes Archiv)
10.5281/zenodo.22175922
DOI (zuletzt registrierte Version)
10.5281/zenodo.22175923

„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.“ Das sind alles Multithreading-Fragen, aber das Problem, das Ausführungsmittel zu wählen, das Problem, gemeinsame Daten zu schützen, und die Probleme von UI und Stoppen müssen getrennt betrachtet werden.

Java hat Multithreading seit JDK 1.0 in die Sprache eingebaut und in java.util.concurrent einen ausgereiften Werkzeugkasten zusammengestellt. JDK 21 hat außerdem virtuelle Threads offiziell eingeführt. Gerade weil die Werkzeuge so reichhaltig sind, liegt der Kern des Entwurfs darin, der Reihe nach zu wählen: was nebenläufig laufen soll, was gemeinsam genutzt wird und wie gestoppt wird.1

Dieser Artikel ist die Java-Edition der Multithreading-Praxisserie. Er richtet sich an Entwickler, die Geschäftssysteme, Batch-Jobs und Serveranwendungen schreiben, nimmt vor allem das LTS-Release JDK 21 und neuer ins Visier und ordnet Grundsätze und Vorbehalte auf der Grundlage von Primärquellen mit Stand August 2026. Er ist so geschrieben, dass er für sich allein gelesen werden kann. Dieselben Grundsätze, für andere Sprachen ausgearbeitet, finden Sie in der „.NET-Edition“, der „C++-Edition“ und der „C-Edition“.

1. Zuerst das Fazit — Ausführung, gemeinsame Nutzung und Stoppen getrennt entwerfen

Virtuelle Threads zu wählen löst weder Konflikte um gemeinsame Daten noch den Stopp-Pfad von allein. Erzeugen Sie in Geschäftscode keine Threads direkt; überlassen Sie die Ausführung der Aufgaben ExecutorService und entwerfen Sie dann in dieser Reihenfolge.2

Was zu entscheiden ist Grundsatz Wo genauer nachlesen
Womit die Arbeit läuft Ein virtueller Thread pro Aufgabe für I/O-Warten, Plattform-Threads in etwa Kernzahl für CPU-Berechnung Kapitel 2
Wie viel angenommen wird Ein Semaphore für die Zahl gleichzeitiger Aufrufe eines externen Dienstes; eine Obergrenze für Kapazität oder Einreichung bei Arbeit, die sich staut Kapitel 2 und 3
Was gemeinsam genutzt wird In Teilergebnisse aufteilen und unveränderliche Daten, nebenläufige Collections und Warteschlangen verwenden Kapitel 3
Wie Zustand geschützt wird Atomic-Klassen für atomare Aktualisierungen eines einzelnen Werts, eine eigene Sperre für zusammengesetzten Zustand. Von volatile keine Atomarität erwarten Kapitel 4
Wie gestoppt wird Kooperatives Stoppen über Interrupts mit einer befristeten Abschlussprüfung kombinieren Kapitel 5 und 6
Wie UI und Untersuchung behandelt werden Die UI auf ihrem eigenen Thread halten. Für Plattform-Threads und virtuelle Threads unterschiedliche Dumps nehmen Kapitel 7 und 8

Wenn Sie von vorn lesen, folgen Sie der Reihenfolge dieser Tabelle. Untersuchen Sie gerade „es stoppt nicht“, beginnen Sie bei Kapitel 5 und 6; bei „es ist eingefroren“ bei Kapitel 8. Die Pinning-Vorbehalte, die sich zwischen JDK 21 bis 23 und JDK 24 und später unterscheiden, stehen in Abschnitt 2.4, die Unterscheidung zwischen Preview und offiziellen Features in Kapitel 9.

In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (28 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle

2. Das Ausführungsmittel wählen — I/O-Warten von CPU-Berechnung trennen

2.1. Die Arbeit an ExecutorService übergeben

Auch in Java lautet die Grundregel, new Thread nicht durch Geschäftscode zu streuen. Trennen Sie die als Runnable / Callable ausgedrückte „Arbeit“ von der „Ausführungsart“ wie Threadzahl und Warteschlange, und überlassen Sie Erzeugung, Wiederverwendung und Entsorgung der Threads ExecutorService.2

Wählen Sie das Ausführungsmittel zuerst danach, was die Arbeit bestimmt.13

Überwiegend I/O-WartenHTTP-Aufrufe, DB, DateienCPU-gebundene BerechnungEs gibt eine parallel auszuführende AufgabeWas bestimmt die Aufgabe?Virtuelle ThreadsExecutors.newVirtualThreadPerTaskExecutor()Einer pro Aufgabe. Niemals poolenFester Pool aus Plattform-ThreadsExecutors.newFixedThreadPool(etwa Kernzahl)oder ein parallel streamGleichzeitige Aufrufe externer Dienstemit einem Semaphore begrenzen, nicht mit einem Pool

Abbildung 1: Für I/O-Warten virtuelle Threads, für CPU-Berechnung einen herkömmlichen Pool. Die Begrenzung gleichzeitiger Zugriffe entwerfen Sie getrennt.

Virtuelle Threads sind keine „schnellen Threads“. Sie erhöhen nicht die Ausführungsgeschwindigkeit der Berechnung selbst; sie sind ein Werkzeug, um große Mengen warteintensiver Arbeit nebenläufig zu behandeln und den Durchsatz zu steigern. Für Berechnungen, die die CPU voll auslasten, verwenden Sie wie bisher einen festen Pool aus Plattform-Threads in etwa Kernzahl oder einen parallel stream.3

2.2. Virtuelle Threads nicht poolen; die Ressourcen, die Sie begrenzen wollen, mit einem Semaphore schützen

Virtuelle Threads sind leichtgewichtige Threads, die von OS-Threads entkoppelt sind. Weil sie den OS-Thread während der vom JDK unterstützten blockierenden Operationen wie I/O, Sperren und sleep freigeben können, erreichen sie genug Nebenläufigkeit, dass eine einzelne JVM Millionen davon handhaben kann. Nicht jede Blockierung gibt ihn frei. Die Bedingungen für Pinning erklärt Abschnitt 2.4 gesondert.3

Die Verwendung lautet ein virtueller Thread pro Aufgabe. Behandeln Sie sie als billig und wegwerfbar; entwerfen Sie nicht um Wiederverwendung, indem Sie virtuelle Threads in einen newFixedThreadPool stecken. Verwenden Sie Executors.newVirtualThreadPerTaskExecutor().13

Andererseits ist eine Grenze wie „höchstens 10 gleichzeitige Verbindungen zur externen API“ weiterhin nötig. Drücken Sie diese Obergrenze mit einem Semaphore aus, nicht mit der Größe eines Pools virtueller Threads. Virtuelle Threads nicht zu poolen und externe Dienste unbegrenzt ansprechen zu dürfen sind zwei verschiedene Dinge.3

Was in einem virtuellen Thread läuft, ist gewöhnlicher synchroner Code. Die Entwurfsphilosophie ist, dass geradliniger Code nach dem Muster „ein Request, ein Thread“ unverändert in großem Maßstab laufen kann, ohne in eine Form wie async/await in .NET umgeschrieben zu werden. Die Form try (var executor = Executors.newVirtualThreadPerTaskExecutor()) steht ebenfalls zur Verfügung, aber prüfen Sie in Abschnitt 6.3, wie lange close() beim Verlassen wartet.12

2.3. Beim Pool aus Plattform-Threads auch eine Obergrenze für die Warteschlange bedenken

„Nicht poolen“ gilt für virtuelle Threads. Java hat seit JDK 5 (2004) ausgereifte Thread-Pools, und sie werden auch heute nach Zweck gewählt.

ThreadPoolExecutor ist ein Allzweck-Pool, dessen Threadzahl, Warteschlange, Ablehnungsrichtlinie und mehr sich im Detail konfigurieren lassen. ForkJoinPool ist ein Work-Stealing-Pool, und sein commonPool() dient als Standardziel asynchroner Ausführung für parallel streams und CompletableFuture. Für periodische Ausführung gibt es ScheduledThreadPoolExecutor. Wo Plattform-Threads wiederverwendet werden, etwa bei CPU-Berechnung, bleiben das wichtige Werkzeuge.

Allerdings begrenzt Executors.newFixedThreadPool die Threadzahl; die Warteschlange ist unbegrenzt. In einem residenten Dienst, in dem die Einreichungen die Verarbeitung dauerhaft überholen, verbrauchen die wartenden Aufgaben und ihre Daten weiter Speicher. Konfigurieren Sie ThreadPoolExecutor entweder mit einer kapazitätsbegrenzten Warteschlange und einer Ablehnungsrichtlinie, oder setzen Sie auf der einreichenden Seite eine Zugangskontrolle wie ein Semaphore, damit Gegendruck entstehen kann. Die Warteschlange in Abschnitt 3.6 folgt demselben Grundsatz.

Ein Pool ist eine Optimierung, um OS-Threads wiederzuverwenden, deren Erzeugung und Halten teuer sind. Virtuelle Threads sind leicht genug, dass Entwickler keinen Grund mehr haben, sie selbst wiederzuverwenden. Das heißt nicht, dass Pools ineffizient geworden wären.

Tatsächlich verwendet der Scheduler des JDK unter den virtuellen Threads einen Work-Stealing-ForkJoinPool und betreibt standardmäßig so viele Carrier-Threads (OS-Threads), wie Prozessoren verfügbar sind. Das Bild, große Mengen nebenläufiger Arbeit mit wenigen OS-Threads zu behandeln, ist dasselbe; die JVM übernimmt die Verwaltung. Es hilft, Java so zu sehen, dass es in dieselbe Richtung geht wie asynchrones I/O in .NET, das während des Wartens keinen Thread belegt hält, und dabei die Form synchronen Codes bewahrt.1

2.4. Pinning nach JDK-Version und nach dem Ort der Blockierung betrachten

Pinning ist der Zustand, in dem ein virtueller Thread seinen Carrier-Thread nicht verlassen kann, sodass auch der OS-Thread weiter wartet. Häufiges oder langes Pinning nagt am Skalierungsvorteil virtueller Threads.3

Wo die Blockierung stattfindet JDK 21 bis 23 JDK 24 und später
Innerhalb eines synchronized-Blocks oder einer synchronized-Methode Pinning tritt auf. Stellen mit häufigen oder langen Blockierungen auf ReentrantLock umzustellen erwägen JEP 491 hat diese Einschränkung beseitigt. Mechanisches Ersetzen allein als Pinning-Gegenmaßnahme ist unnötig
Während der Ausführung nativen Codes (JNI) oder einer Foreign Function Der Carrier-Thread kann unfreigegeben bleiben Getrennt von der Verbesserung bei synchronized bleibt Pinning an der nativen Grenze bestehen

Was JEP 491 in JDK 24 beseitigt hat, ist das synchronized-Pinning, das aus der Monitor-Implementierung kam. Wenn Sie große Mengen Operationen laden, die in JNI-Treibern oder Geräte-APIs lange blockieren, bleibt das Problem, die Carrier-Threads zu erschöpfen. Gehen Sie nicht davon aus, „es ist JDK 24, also darf überall gewartet werden“. Prüfen Sie auch, ob interne Leitlinien noch beim Vorbehalt aus der JDK-21-Zeit stehen.43

3. Gemeinsam genutzten veränderlichen Zustand verringern — die gemeinsame Nutzung prüfen, bevor Sie Synchronisierung schreiben

3.1. Auch count++ verliert Inkremente, wenn Operationen sich überlappen

Eine Race Condition ist ein Fehler, bei dem das Ergebnis davon abhängt, in welcher Reihenfolge mehrere Threads den Code erreichen. count++ sieht wie ein einziger Ausdruck aus, zerfällt aber in Lesen, Addieren und Zurückschreiben. Rutscht ein anderer Thread dazwischen, geht eines der Inkremente verloren.

Thread BGemeinsame Variable countThread AThread BGemeinsame Variable countThread Acount = 10Zweimal inkrementiert, und doch count = 11Das Inkrement von Thread A ging verlorenLesen (10)Lesen (10)Lokal addieren (11)Lokal addieren (11)Zurückschreiben (11)Zurückschreiben (11)

Abbildung 2: Lesen zwei Threads denselben Wert und schreiben ihn zurück, bleibt von zwei Inkrementen nur eines übrig.

Der andere Klassiker ist der Deadlock, bei dem Threads auf die Sperren des jeweils anderen warten und nicht weiterkommen. Das behandelt Abschnitt 4.3. Beides hängt vom Timing ab, deshalb kann das, was auf einer Entwicklungsmaschine selten ist, in der Produktion mit anderer Kernzahl und Last häufig auftreten. Es reproduziert sich auch nicht mehr, sobald Debugger oder Protokollierung dazukommen, weil die Beobachtung das Timing ändert.

Genau deshalb beginnen Sie damit, die Stellen zu verringern, die Synchronisierung brauchen, bevor Sie korrekt synchronisieren. Ist das Laufen auf mehreren Threads eine Anforderung, soll der Entwurf gemeinsam genutzte veränderliche Daten verringern.

3.2. Das Java Memory Model legt fest, wann andere Threads einen Schreibvorgang sehen

In Java ist die Sichtbarkeit gemeinsam genutzter Daten über die happens-before-Beziehung des Java Memory Model (JMM) definiert. Der Zugriff auf eine gemeinsame Variable ohne Synchronisierung ist kein undefiniertes Verhalten wie in C++, aber ein veralteter Wert kann sichtbar bleiben, oder Schreibvorgänge können in vertauschter Reihenfolge erscheinen.5

Prüft eine Schleife beispielsweise nur ein boolean-Flag, kann ein von einem anderen Thread geänderter Wert dauerhaft unsichtbar bleiben. Das ist kein JVM-Fehler, sondern ein Speicherkonsistenzfehler, der entsteht, weil die nötige Synchronisierung fehlt.

Was happens-before herstellt, sind synchronized, volatile und die Klassen in java.util.concurrent. Die nebenläufigen Collections garantieren außerdem als Teil ihrer Spezifikation die Beziehung zwischen einer Aktualisierung und einem späteren Abruf dieses Werts. Statt mit einer nackten gemeinsamen Variablen zu tricksen, nutzen Sie die Garantien dieser Werkzeuge. Sichtbarkeit eines Werts und die Ausführung mehrerer Operationen als eine Einheit sind allerdings verschiedene Dinge. Den Unterschied zur Atomarität legt Abschnitt 4.1 dar.56

3.3. Aggregation in Teilergebnisse aufteilen und am Ende zusammenführen

Bei paralleler Aggregation denken Sie zuerst daran, dass jeder Thread ein Teilergebnis aufbaut und sie am Ende summiert werden, statt dass jeder Thread in eine einzige Summenvariable schreibt. Die Operationen reduce / collect von parallel streams stellen diese Struktur als Rahmen bereit.

LongAdder, für hochfrequente Inkremente verwendet, ist ebenfalls eine Strategie, Aktualisierungen über interne Zellen zu verteilen und sie beim Lesen zu summieren. Verringern Sie zuerst Schreibvorgänge auf gemeinsamen Zustand, und wählen Sie danach die nötige Synchronisierung.

3.4. Wenn Sie unveränderlich machen, prüfen Sie bis in die Elemente

Konfiguration und Stammdaten teilen Sie als unveränderliche Daten, die nach dem Aufbau nicht mehr umgeschrieben werden. Zum Austauschen ist das bewährte Muster, ein neues Objekt zu bauen und eine volatile-Referenz umzuhängen.

Allerdings machen record oder List.copyOf allein nicht die gesamten Daten unveränderlich. Die Accessoren eines record geben die Referenzen auf ihre Bestandteile unverändert zurück. Selbst wenn List.copyOf / Map.copyOf die Collection unveränderbar machen, werden die Elementobjekte nicht tief kopiert.

Sind die Elemente veränderlich, kann anderer Code, der eine Referenz auf dasselbe Element hält, den Inhalt umschreiben, und der Konflikt bleibt. Um sie ohne Synchronisierung als unveränderliche Daten zu teilen, bestätigen Sie, dass der gesamte Objektgraph einschließlich der Elemente unveränderlich ist. Sind veränderliche Elemente enthalten, schneiden Sie die gemeinsame Nutzung mit einer tiefen Kopie ab, oder führen Sie die Elemente ebenfalls auf record / unveränderliche Typen. Unterscheiden Sie „sieht schreibgeschützt aus“ von „ist unveränderlich“.

3.5. Die zusammengesetzten Operationen von ConcurrentHashMap nutzen und den Umfang ihrer Garantie kennen

Für „anlegen und einfügen, wenn der Schlüssel fehlt“ schreiben Sie Prüfung und Einfügen nicht getrennt; verwenden Sie ConcurrentHashMap.computeIfAbsent. Der gesamte Methodenaufruf läuft atomar, und fehlt der Schlüssel, wird die Mapping-Funktion innerhalb dieses einen Aufrufs genau einmal aufgerufen. Die Garantie unterscheidet sich von .NETs ConcurrentDictionary.GetOrAdd, dessen Factory unter Konflikt mehr als einmal laufen kann.6

Für einen Häufigkeitszähler ist die folgende Form das bewährte Muster.

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

„Einmal in diesem Aufruf“ und „einmal in der Lebensdauer dieses Schlüssels“ sind verschieden. Gibt die Funktion null zurück oder wirft sie eine Ausnahme, wird nichts registriert, und ein späterer Aufruf führt sie erneut aus. Dasselbe gilt, wenn ein registrierter Eintrag entfernt wird. Für eine Initialisierung, die verdoppelte Seiteneffekte nicht duldet, entwerfen Sie sie so, dass sie mit einem Nicht-null-Wert erfolgreich ist, und beziehen Sie ein, wie die Einträge behandelt werden.6

Außerdem werden im Tausch gegen die atomare Ausführung während der Berechnung einige Aktualisierungen anderer Threads blockiert. Halten Sie die Mapping-Funktion kurz und einfach, und aktualisieren Sie diese Map niemals aus ihrem Inneren. Eine erkennbare rekursive Aktualisierung kann zu einer IllegalStateException führen.6

3.6. Für die Übergabe zwischen Threads eine kapazitätsbegrenzte Warteschlange verwenden

Statt dass mehrere Threads die Daten direkt berühren, übergeben Sie sie über eine BlockingQueue. Bei einem ArrayBlockingQueue mit angegebener Kapazität blockiert put, wenn die Warteschlange voll ist, und erzeugt so natürlichen Gegendruck auf der erzeugenden Seite.

Das ist dieselbe Struktur wie der begrenzte Kanal in der .NET-Edition. Auch mit virtuellen Threads bleibt ein Entwurf wirksam, der die Grenze zwischen Produzent und Konsument und die Obergrenze des Rückstands ausdrücklich macht.

4. Den verbleibenden gemeinsamen Zustand schützen — volatile, Atomic-Klassen und Sperren unterscheiden

4.1. Sichtbarkeit und Atomarität sind getrennte Garantien

volatile ist ein Werkzeug für Sichtbarkeit und Ordnung, also für happens-before. Es ist keine Garantie, dass „lesen, berechnen, zurückschreiben“ als Einheit geschieht, deshalb verhindert ++ auf einem volatile int von mehreren Threads aus das verlorene Inkrement aus Abbildung 2 nicht.5

Was zu schützen ist Zu wählendes Werkzeug Vorbehalt
Einfache Zustandsbenachrichtigung, Umhängen einer Referenz auf unveränderliche Daten volatile Keine Atomarität für zusammengesetzte Operationen
Atomare Aktualisierung eines einzelnen Werts AtomicInteger / AtomicLong / AtomicReference Für Statistiken, die mit hoher Frequenz inkrementiert werden, auch LongAdder verwenden
Konsistenz über mehrere Werte Eine Sperre Dieselbe Disziplin überall einhalten, wo dieselben Daten berührt werden

Wie in der .NET- und der C++-Edition weisen Sie atomare Aktualisierungen den Atomic-Klassen und zusammengesetzten Zustand den Sperren zu. Entscheidend ist, etwas nicht mit volatile allein threadsicher machen zu wollen.

4.2. Sperren den Daten zuordnen, die sie schützen, nicht Codeabschnitten

Ordnen Sie jedem Satz veränderlicher Daten, den Sie schützen wollen, ein eigenes Sperrobjekt zu. Nehmen Sie dann überall, wo die Daten berührt werden, dieselbe Sperre. Machen Sie im Review nachprüfbar, welche Sperre welche Daten schützt.

Vermeiden Sie synchronized(this), synchronized(SomeClass.class) und das Sperren öffentlich sichtbarer Objekte. Externer Code kann dasselbe Objekt sperren, und unbeabsichtigte Kollisionen entstehen. Verwenden Sie ein nach außen nie sichtbares private final Object lock = new Object(); oder ein eigenes ReentrantLock.

4.3. Externe Arbeit während des Haltens einer Sperre vermeiden und die Erwerbsreihenfolge festlegen

I/O, Listener-Aufrufe oder unbekannten Code auszuführen, während eine Sperre gehalten wird, verlängert die Haltezeit. Nimmt der Aufgerufene eine andere Sperre, kann außerdem ein zyklisches Warten entstehen.

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

Abbildung 3: Hält der eine Thread Sperre 1 und wartet auf Sperre 2, während der andere in umgekehrter Reihenfolge wartet, kommt keiner weiter.

Wo mehrere Sperren genommen werden, legen Sie die Erwerbsreihenfolge für alle Threads fest. Wo die Reihenfolge nicht garantiert werden kann, stellen Sie mit tryLock(timeout) einen Pfad bereit, der die gehaltenen Sperren freigibt und erneut versucht, wenn die Sperre nicht zu bekommen ist. Halten Sie die beiden Regeln zusammen: keine lange oder externe Arbeit, während eine Sperre gehalten wird, und eine einheitliche Erwerbsreihenfolge.

4.4. synchronized für kurzen Ausschluss, ReentrantLock wenn mehr nötig ist

Für kurzen, einfachen Ausschluss reicht synchronized. Wählen Sie ReentrantLock, wenn Sie zeitlich begrenzten Erwerb mit tryLock(timeout), eine Fairness-Policy, mehrere Conditions oder Erwerb und Freigabe über verschiedene Methoden verteilt brauchen.

Bei gewöhnlicher Verwendung von ReentrantLock dürfen Sie die Form try unmittelbar nach lock(), mit unlock() in finally nicht aufbrechen. Erwarten Sie nicht, dass die Sperre wie bei RAII in C++ automatisch freigegeben wird; strukturieren Sie so, dass die Sperre auch bei einer Ausnahme freigegeben wird.

In Kombination mit virtuellen Threads prüfen Sie auch die JDK-Unterschiede in Abschnitt 2.4. Ab JDK 24 brauchen Sie synchronized nicht pauschal allein als Pinning-Gegenmaßnahme zu ersetzen. Die Disziplin, Sperren kurz zu halten, ändert sich jedoch nicht.4

5. Festlegen, wie Aufgaben stoppen — Interrupts nicht verschlucken

5.1. interrupt ist ein Signal für kooperatives Stoppen, keine Zwangsbeendigung

Entwerfen Sie Stoppen und Abbrechen in Java um kooperatives Stoppen über Interrupts. t.interrupt() setzt den Interrupt-Status des Zielthreads. Ist der Thread in sleep / wait / join oder Ähnlichem blockiert, reißt eine InterruptedException ihn aus dem Warten, und der Interrupt-Status wird an diesem Punkt zurückgesetzt.7

In der JDK-21-API, auf die sich dieser Artikel bezieht, werfen die früheren Zwangsmittel Thread.stop / suspend / resume UnsupportedOperationException. Statt zu gefährlichen Mechanismen zurückzukehren, die Sperren in einem inkonsistenten Zustand freigeben oder Deadlocks einladen, soll die Aufgabe selbst auf das Stopp-Signal antworten und enden.7

Kann selbst endenKann nicht enden (in einer Bibliothek usw.)Stoppende Seite ruft t.interrupt() aufInterrupt-Status wird gesetztThread in der Berechnungprüft Thread.interrupted() in seiner SchleifeBlockiert in sleep / wait / joinInterruptedException fliegt und weckt sofort(der Status wird zurückgesetzt)Räumt auf und endet selbstWas tut der catch?Thread.currentThread().interrupt()stellt den Status wieder her und hinterlässt das Signal

Abbildung 4: Die Aufgabe, die den Interrupt empfängt, räumt auf und endet selbst. Die Ausnahme zu verschlucken verliert das Stopp-Signal.

5.2. Die Verantwortung nach dem Empfang von InterruptedException ausdrücklich machen

Schreiben Sie keinen Code, der InterruptedException fängt und nichts tut. Können Sie innerhalb Ihrer eigenen Verantwortung fertig werden, räumen Sie auf und beenden. Überlassen Sie die Entscheidung dem Aufrufer, werfen Sie die Ausnahme unverändert weiter oder stellen Sie den Status mit Thread.currentThread().interrupt() wieder her, um das Signal zu hinterlassen.7

Auch eine Berechnungsschleife muss den Interrupt prüfen und zum Beenden übergehen, wie in Abbildung 4. Nur die Seite zu implementieren, die die Stopp-Anforderung sendet, stoppt nichts, wenn die empfangende Seite sie ignoriert. Diese Eigenschaft gilt unverändert für shutdownNow() im nächsten Kapitel.

6. ExecutorService herunterfahren — Anforderung und Abschlussprüfung trennen

6.1. shutdownNow allein aufzurufen heißt nicht, dass das Herunterfahren abgeschlossen ist

Beim Herunterfahren eines ExecutorService trennen Sie die Operation, die die Annahme stoppt, die Operation, die Abbruch anfordert, und die Operation, die auf den Abschluss wartet.2

API Rolle Was sie allein nicht garantiert
shutdown() Stoppt die Annahme neuer Aufgaben und lässt bereits eingereichte Aufgaben laufen Der aufrufende Thread wartet nicht auf den Abschluss
awaitTermination(...) Wartet nach einer Shutdown-Anforderung bis zum Abschluss, zur Frist oder zu einem Interrupt, je nachdem, was zuerst eintritt Den Abschluss des Herunterfahrens bei Zeitüberschreitung
shutdownNow() Versucht, laufende Aufgaben zu stoppen, und gibt die wartenden Aufgaben zurück, die nie liefen Die Zwangsbeendigung laufender Aufgaben oder das Warten, bis sie enden
close() Stoppt die Annahme und wartet, bis der Executor endet Eine Obergrenze für die Wartezeit

shutdownNow() ist Best-Effort. Standardimplementierungen wie ThreadPoolExecutor brechen typischerweise über Interrupts ab, deshalb stoppt eine Aufgabe, die nicht auf Interrupts reagiert, nicht. Verwenden Sie einen eigenen Executor, prüfen Sie dessen Abbruchmethode, einschließlich der Frage, ob überhaupt ein Interrupt gesendet wird.2

Um eine einzelne Aufgabe abzubrechen, verwenden Sie Future.cancel(true). Verwechseln Sie auch hier eine über Interrupt zugestellte Stopp-Anforderung an eine laufende Aufgabe nicht damit, dass die Aufgabe ihre Arbeit tatsächlich beendet.

6.2. Ein befristetes zweistufiges Herunterfahren verwenden

Stoppen Sie zuerst die Annahme und warten Sie auf den vollständigen Durchlauf; ist die Frist vorbei, fordern Sie den Abbruch an und warten erneut auf den Abschluss. Endet er trotzdem nicht, melden Sie das dem Aufrufer in einer von Erfolg unterscheidbaren Form. Das folgende Beispiel baut auf dem offiziellen zweistufigen Muster auf und ergänzt Erfolg oder Misserfolg des Herunterfahrens sowie die Behandlung nicht ausgeführter Futures.2

/** Gibt true zurück, sobald das Herunterfahren abgeschlossen ist. Solange dies false ist, gemeinsame Ressourcen nicht freigeben. */
boolean shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown();                    // Stufe 1: Annahme neuer Aufgaben stoppen und auf Abschluss warten
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            // Stufe 2: Abbruch anfordern. Die ohne Ausführung abgelegten Aufgaben werden zurückgegeben,
            // daher ihre 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 unvollständig. In einer von Erfolg unterscheidbaren Form 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 dieses Threads wiederherstellen
        return false;                   // Auch auf diesem Pfad kann das Herunterfahren unvollständig sein
    }
}
Innerhalb der Frist abgeschlossenZeitüberschreitungAbgeschlossenImmer noch nicht fertigshutdown()Annahme neuer Aufgaben stoppenawaitTerminationwartet auf den AbschlussHerunterfahren abgeschlossenshutdownNow()sendet Interrupt an laufende Aufgaben(ob sie reagieren, liegt an der Aufgabe)awaitTerminationwartet erneutAls Anomalie aufzeichnen(eine Aufgabe, die Interrupts ignoriert, ist verdächtig)

Abbildung 5: Die Stufe, die auf den Abschluss wartet, von der Stufe trennen, die Abbruch anfordert und erneut wartet. Endet es trotzdem nicht, als unvollständiges Herunterfahren behandeln.

Geben Sie die gemeinsamen Ressourcen, die die Aufgaben verwenden, nicht frei, solange der Rückgabewert false ist. Nicht nur wenn die zweite Wartezeit abläuft, sondern auch auf dem Pfad, auf dem die wartende Seite unterbrochen wurde, können Aufgaben noch laufen. Protokollieren Sie das nicht bloß und behandeln Sie es als Erfolg; lassen Sie den Aufrufer feststellen, dass das Herunterfahren unvollständig ist.

6.3. close und try-with-resources nur in Bereichen verwenden, die vollständig durchlaufen können

Ab JDK 19 lässt sich ExecutorService als AutoCloseable verwenden. try (var executor = ...) wartet beim Verlassen des Bereichs mit close() auf die Beendigung. Es funktioniert auch in Kombination mit einem Executor virtueller Threads.2

Allerdings wartet close() ohne Timeout, deshalb ist es kein Ersatz für das befristete zweistufige Muster. Gibt es eine Aufgabe, die nie endet oder nicht auf Interrupts reagiert, wartet auch der Thread, der zu schließen versucht, für immer.

Verwenden Sie try-with-resources für einen endlichen Bereich, in dem Aufgaben an Ort und Stelle eingereicht und ihr Abschluss an Ort und Stelle abgewartet werden kann. Auf Pfaden, auf denen endloses Warten keine Option ist, etwa beim Herunterfahren der gesamten Anwendung, entwerfen Sie eine befristete Wartezeit und den Umgang mit einem unvollständigen Herunterfahren.2

6.4. Die Aufgabe in der Warteschlange und das Future des Aufrufers sind nicht unbedingt dasselbe

Was shutdownNow() zurückgibt, sind die Objekte, die in der Ausführungswarteschlange geblieben sind. Bei einem einfachen submit ist das normalerweise der FutureTask selbst, der dem Aufrufer übergeben wurde, sodass das cancel(false) im Beispiel Aufrufer wecken kann, die auf get() warten.

Werden Aufgaben über einen Wrapper wie ExecutorCompletionService eingereicht, kommt dagegen der Wrapper in der Warteschlange zurück, ein anderes Objekt als das Future des Aufrufers. Es gibt Konfigurationen, in denen das obige Beispiel allein das Future des Aufrufers nicht abschließen kann.

Halten Sie in diesem Fall bei der Einreichung eine Liste der aufruferseitigen Futures und brechen Sie diese beim Herunterfahren ab, oder entwerfen Sie so, dass aus der Warteschlange abgelegte Aufgaben an ihre Besitzer zurückgegeben werden. Prüfen Sie den Stopp-Pfad bis zu „der Seite, die auf Arbeit wartet, von der nun feststeht, dass sie nie läuft“.

7. Die UI auf ihren eigenen Thread zurückbringen — Swings EDT

Die UI von Swing verwaltet der Event Dispatch Thread (EDT). Die Methoden von Swing-Komponenten sind in der Regel nicht threadsicher, und sie von mehreren Threads aus zu berühren lädt Thread-Interferenz und Speicherkonsistenzfehler ein.8

Um die Anzeige von einem anderen Thread aus zu aktualisieren, beauftragen Sie den EDT mit SwingUtilities.invokeLater. Umgekehrt lässt lange Verarbeitung auf dem EDT die UI einfrieren, deshalb lagern Sie schwere Arbeit mit SwingWorker oder Ähnlichem auf einen Worker-Thread aus. Die Arbeit nach außen zu geben und die Anzeige des Ergebnisses auf den UI-Thread zurückzubringen gehören zusammen.8

Dieselbe Struktur gilt in JavaFX. Beauftragen Sie UI-Aktualisierungen mit Platform.runLater beim Anwendungs-Thread. Wie viele Threadarten es auch gibt, der Grundsatz, dass die UI ausschließlich dem Thread gehört, der sie verwaltet, ändert sich nicht.

8. Prüfung und Untersuchung vorbereiten — Design-Review, Dumps und Lasttests

8.1. Zuerst gemeinsame Daten und Stopp-Pfad prüfen

Selbst wenn gewöhnliche Tests durchlaufen, kann es nur sein, dass zufällig kein Konflikt auftrat. Erwarten Sie nicht, Race-Fehler allein durch Tests zu finden; legen Sie die erste Verteidigungslinie in den Entwurf.

Prüfen Sie in einer Zuordnungstabelle die gemeinsam genutzten veränderlichen Daten, die Sperre, die jedes Stück schützt, und die Erwerbsreihenfolge der Sperren. Prüfen Sie außerdem, dass kein catch InterruptedException verschluckt und dass Shutdown- und Interrupt-Pfade jede Aufgabe erreichen. Machen Sie den Entwurf der Kapitel 3 bis 6 direkt zu Review-Punkten.

8.2. Den Dump nehmen, der zur Threadart passt

Den Zustand von Plattform-Threads untersuchen Sie mit jstack oder jcmd <pid> Thread.print. Die Option -l liefert zusätzlich Informationen zu Sperren.9

Allerdings enthält das herkömmliche Dump-Format die virtuellen Threads der Anwendung nicht. Wenn Sie eine stehen gebliebene Anfrage in einem Aufbau mit virtuellen Threads verfolgen, erfassen Sie mit jcmd <pid> Thread.dump_to_file -format=json <Datei> ein Format, das virtuelle Threads einschließt.1

Bei der Untersuchung eines Hangs nehmen Sie im Abstand von wenigen Sekunden zwei bis drei Dumps und vergleichen die Threads, die sich nicht bewegen. Bei einem Plattform-Thread, der auf eine Sperre wartet, verfolgen Sie, worauf er wartet und wer diese Sperre hält. Bei virtuellen Threads sehen Sie im Dump, der sie einschließt, wo in der Verarbeitung sie stehen geblieben sind. Das Protokollieren von Zeitüberschreitungen bei tryLock(timeout) gibt Ihnen außerdem einen Anlass, einen Dump zu nehmen.

8.3. Die Ausführungsreihenfolge unter produktionsnaher Last durcheinanderschütteln

Zusätzlich zu Dumps als zweiter Verteidigungslinie führen Sie Stresstests als dritte durch. Lange mit mehr Parallelität als Kernen laufen, die Verarbeitungsreihenfolge randomisieren und künstliche Verzögerungen einfügen machen es leichter, die Ausführungsreihenfolgen zu treffen, in denen Konflikte entstehen.

Führen Sie vor der Veröffentlichung mindestens einmal einen Test mit produktionsnaher Datenmenge und Threadzahl durch. Behandeln Sie das Bestehen eines Lasttests jedoch nicht als Grund, den Entwurf des gemeinsamen Zustands zu überspringen.

9. Wo die neuen APIs stehen — Preview und offizielle Features trennen

Dieses Kapitel gibt den Stand vom August 2026 wieder. Es trennt die bereits verfügbaren grundlegenden Werkzeuge von APIs, die noch in der Entwicklung sind, damit beides nicht vermischt wird.

Structured Concurrency (StructuredTaskScope) ist eine API, die mehrere zusammengehörige Teilaufgaben als eine Arbeitseinheit behandelt und Fehlerweitergabe sowie Abbruch strukturiert. Sie ist für die Verwendung mit virtuellen Threads gedacht, ist an diesem Punkt aber noch ein Preview-Feature. Die fünfte Preview in JDK 25 (JEP 505) hat die API um StructuredTaskScope.open() neu geformt, und sie setzt sich in JDK 26 als sechste Preview (JEP 525) fort.1011

Scoped Values dagegen, die das Teilen unveränderlichen Kontexts behandeln, wurden in JDK 25 finalisiert. Sie sind eine Lösung für die Probleme von ThreadLocal wie Veränderlichkeit, Lebensdauerverwaltung und Vererbungskosten.12

Die Richtung, in die die neuen APIs zielen, ist dieselbe wie die Grundsätze dieses Artikels: Aufgabengrenzen ausdrücklich machen, gemeinsame Nutzung zur Unveränderlichkeit hin schieben und Stoppen kooperativ behandeln.

10. Zusammenfassung — 10 Punkte zur Prüfung vor der Implementierung und im Review

# Was zu prüfen ist
1 Vermeidet Geschäftscode, new Thread direkt zu erzeugen, und übergibt Aufgaben an ExecutorService?
2 Erhalten I/O-Warten und CPU-Berechnung unterschiedliche Ausführungsmittel, und ist auch für die Warteschlange des festen Pools eine Obergrenze oder Gegendruck bedacht?
3 Bleiben virtuelle Threads ungepoolt, und werden gleichzeitige Aufrufe externer Dienste durch ein Semaphore begrenzt?
4 Werden gemeinsame Daten zu Aufteilen, Unveränderlichkeit und Übergabe hingeführt, und sind auch die Elemente von records und unveränderlichen Collections geprüft?
5 Sind eigene Sperren den Daten zugeordnet, unter Vermeidung von synchronized(this) und Sperren auf öffentliche Objekte?
6 Werden zusammengesetzte Operationen wie computeIfAbsent verwendet, unter Beachtung der Bedingungen, unter denen die Mapping-Funktion erneut läuft, und kurz gehalten?
7 Wird von volatile keine Atomarität erwartet, mit Zählern bei den Atomic-Klassen oder LongAdder und zusammengesetztem Zustand bei Sperren?
8 Wird InterruptedException nie verschluckt, sodass das Stopp-Signal die Aufgabenseite erreicht?
9 Ist das Herunterfahren ein befristetes zweistufiges Muster? Ist close() auf Bereiche beschränkt, die den Abschluss garantieren können, und werden unvollständiges Herunterfahren und nicht ausgeführte Futures behandelt?
10 Sind UI-Aktualisierungen von Swing / JavaFX auf dem EDT / Anwendungs-Thread gebündelt?

Javas virtuelle Threads haben einen Weg eröffnet, geradlinigen synchronen Code unverändert zu skalieren. Dennoch sind das Werkzeug für Durchsatz, die Werkzeuge zum Schutz gemeinsamen Zustands und das Werkzeug, um einen Stopp zu signalisieren, verschieden. Mit getrennten Rollen für Ausführung, gemeinsame Nutzung und Stoppen zu wählen ist die Grundlage 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. 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 herkömmliche Thread-Dumps keine virtuellen Threads enthalten. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. 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, keine über Best-Effort hinausgehende Garantie besteht und eine nicht auf Interrupts reagierende Aufgabe nicht endet; dazu, dass sich mit awaitTermination auf den Abschluss warten lässt; dazu, dass close() (Java 19 und später, AutoCloseable) shutdown aufruft und auf den Abschluss wartet und sich mit try-with-resources verwenden lässt; sowie dazu, dass das zweistufige Herunterfahren shutdown, dann awaitTermination, dann shutdownNow als Anwendungsbeispiel gezeigt wird. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  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 (newVirtualThreadPerTaskExecutor); dazu, dass für die Begrenzung der Nebenläufigkeit ein Semaphore statt eines Thread-Pools verwendet wird; 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 virtuelle Threads neu geschrieben wurde, sodass eine Blockierung innerhalb eines synchronized-Blocks oder einer synchronized-Methode einen virtuellen Thread nicht mehr an seinen Carrier-Thread pinnt; sowie dazu, dass die aus der JDK-21-bis-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, 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 erkennbare rekursive Aktualisierung zu IllegalStateException führt; sowie dazu, dass Abrufoperationen (get) nicht blockieren und zwischen einer Aktualisierung für einen bestimmten Schlüssel und einem nachfolgenden Abruf eine happens-before-Beziehung besteht. ↩ ↩2 ↩3 ↩4

  7. 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 und beschädigte Objekte werden sichtbar; suspend lädt Deadlocks ein) und daher zur Entfernung 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

  8. 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-Interferenz 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

  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 Structured Concurrency 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 offizielles Feature. ↩

  11. OpenJDK, JEP 525: Structured Concurrency (Sixth Preview). Dazu, dass Structured Concurrency 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, Lebensdauerverwaltung und 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 große Mengen überwiegend auf I/O wartender Aufgaben laufen zu lassen; sie machen Code nicht schneller, sondern erhöhen den Durchsatz. Für I/O-gebundene Arbeit verwenden Sie pro Aufgabe einen virtuellen Thread (Executors.newVirtualThreadPerTaskExecutor) und poolen 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). Und wenn Sie die Zahl gleichzeitiger Zugriffe auf einen externen Dienst begrenzen wollen, empfiehlt sich im Zeitalter der virtuellen Threads eine Begrenzung über Semaphore statt über einen Pool.
Sollte ich synchronized oder ReentrantLock verwenden?
Für kurzen, einfachen Ausschluss reicht synchronized, und der Code bleibt knapp. Wählen Sie ReentrantLock, wenn Sie Funktionen wie zeitlich begrenzten Erwerb über tryLock, eine Fairness-Policy oder mehrere Conditions brauchen. In Kombination mit virtuellen Threads gibt es einen historischen Vorbehalt: In JDK 21 bis 23 pinnt eine Blockierung innerhalb eines synchronized-Blocks den virtuellen Thread an seinen OS-Thread, weshalb Stellen mit häufigen oder langen Blockierungen auf ReentrantLock umzustellen empfohlen wurde. JDK 24 (JEP 491) hat die Monitor-Implementierung neu geschrieben und diese Einschränkung beseitigt. 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 Inkremente verloren. Verwenden Sie für Zähler AtomicInteger / AtomicLong (bei hochfrequenter Aggregation LongAdder) und eine Sperre, wenn mehrere Variablen gemeinsam geschützt werden müssen. volatile eignet sich praktisch nur dort, wo ein Thread schreibt und die anderen nur lesen, etwa bei einem einfachen Zustandsflag.
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, bei denen shutdown wirkungslos bleibt oder shutdownNow ignoriert wird.
Kann ich einen Thread nicht mit Thread.stop anhalten?
Nein. Thread.stop ist grundsätzlich unsicher (Sperren werden in einem inkonsistenten Zustand freigegeben, sodass beschädigte Objekte für andere Threads sichtbar werden) und galt daher 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 kooperatives Stoppen ü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 den Standardimplementierungen ü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