Java Threads verstehen: Plattform-Threads, Virtual Threads und sichere Nebenläufigkeit

Von · Softwareentwickler und Autor von tiny-tool.de

Threads ermöglichen mehreren Aufgaben, innerhalb eines Java-Prozesses nebenläufig zu arbeiten. Entscheidend ist dabei nicht nur, wie du einen Thread startest, sondern wie du Aufgaben trennst, gemeinsamen Zustand schützt und zwischen Plattform- und Virtual Threads auswählst.

Das Wichtigste vorab

Kurzfassung: ca. 1 Minute · vollständiger Artikel: ca. 9 Minuten

  • Ein Thread ist ein Ausführungspfad innerhalb eines Prozesses. Threads teilen sich den Heap und können deshalb gemeinsam auf Objekte zugreifen.
  • start() plant einen neuen Thread zur Ausführung ein; ein direkter Aufruf von run() bleibt im aktuellen Thread.
  • Runnable beschreibt eine Aufgabe. In Anwendungsprogrammen ist diese Trennung meist flexibler als eine eigene Unterklasse von Thread.
  • Gemeinsam veränderter Zustand braucht eine klare Synchronisationsstrategie, etwa Unveränderlichkeit, synchronized, Locks oder atomare Klassen.
  • Executors verwalten Aufgaben und Threads. Virtual Threads eignen sich besonders für sehr viele blockierende I/O-Aufgaben, beschleunigen aber keine CPU-intensive Berechnung.
  • Unterbrechung ist kooperativ: Ein Thread sollte auf InterruptedException sinnvoll reagieren und den Interrupt nicht still verschlucken.
Leuchtende Ausführungspfade verzweigen sich von einem zentralen Rechenkern zu Plattform- und Virtual Threads.
KI-generierte Illustration zu Plattform-Threads, Virtual Threads und sicherer Nebenläufigkeit in Java.

Nebenläufigkeit wird schnell missverständlich: Zwei Aufgaben können sich zeitlich überlappen, ohne in jedem Moment wirklich parallel auf verschiedenen Prozessorkernen zu laufen. Gleichzeitig bringt das Teilen von Daten neue Fehlerklassen mit sich. Dieser Guide führt deshalb vom ersten Thread über Zustände und Synchronisation bis zu Executor Services, Virtual Threads und einem kurzen Kotlin-Vergleich.

Prozess, Thread und Nebenläufigkeit

Ein laufendes Java-Programm ist ein Prozess. Innerhalb dieses Prozesses können mehrere Threads existieren. Jeder Thread besitzt unter anderem einen eigenen Aufrufstapel, aber die Threads eines Prozesses teilen sich den Heap. Dadurch können sie dieselben Objekte sehen und verändern.

Dieses gemeinsame Arbeiten ist leistungsfähig, aber nicht automatisch sicher. Wenn zwei Threads denselben Wert gleichzeitig lesen und schreiben, kann das Ergebnis von der zufälligen zeitlichen Reihenfolge abhängen. Solche Fehler sind häufig schwer reproduzierbar.

Nebenläufigkeit und Parallelität
Begriff Bedeutung Beispiel
Nebenläufigkeit Mehrere Aufgaben machen im selben Zeitraum Fortschritt. Eine Aufgabe wartet auf eine Datei, während eine andere eine Anfrage vorbereitet.
Parallelität Mehrere Aufgaben werden im selben Moment auf unterschiedlichen Recheneinheiten ausgeführt. Zwei CPU-intensive Berechnungen laufen auf zwei Prozessorkernen.

Mehr Threads bedeuten nicht automatisch mehr Geschwindigkeit. Bei CPU-intensiver Arbeit begrenzen die verfügbaren Kerne den sinnvollen Parallelismus; zu viele Plattform-Threads erzeugen zusätzlich Verwaltungs- und Kontextwechselkosten.

Die Zustände eines Java-Threads

Die JVM beschreibt den Zustand eines Threads mit Thread.State. Diese Zustände sind JVM-Zustände und nicht identisch mit den Zuständen, die ein Betriebssystem intern verwendet.

Die sechs Thread-Zustände der JVM
Zustand Bedeutung
NEW Das Thread-Objekt wurde erzeugt, aber noch nicht gestartet.
RUNNABLE Der Thread läuft in der JVM oder wartet darauf, Rechenzeit beziehungsweise eine Betriebssystemressource zu erhalten.
BLOCKED Der Thread wartet auf den Monitor eines synchronized-Bereichs.
WAITING Der Thread wartet ohne Zeitgrenze auf eine Aktion eines anderen Threads, etwa bei join().
TIMED_WAITING Der Thread wartet höchstens bis zu einem bestimmten Zeitpunkt, etwa bei sleep().
TERMINATED Die Methode run() ist normal oder mit einer nicht behandelten Ausnahme beendet.

Einen Thread richtig starten

Für eine einzelne Aufgabe genügt ein Runnable. Da es ein funktionales Interface ist, kannst du eine Lambda-Expression verwenden. Der moderne Thread-Builder macht zugleich sichtbar, ob du einen Plattform- oder Virtual Thread erzeugst.

public class ThreadStartDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> System.out.println(
                "Läuft in: " + Thread.currentThread().getName());

        Thread worker = Thread.ofPlatform()
                .name("report-worker")
                .start(task);

        worker.join();
        System.out.println("Aufgabe beendet");
    }
}

Der Haupt-Thread wartet mit join(), bis worker beendet ist. Ohne dieses Warten kann die Reihenfolge der Ausgaben variieren. Ein nicht gestarteter Thread kann nur einmal mit start() gestartet werden.

start(), run() und join()

start()

Plant den Thread zur Ausführung ein. Die JVM ruft dessen run()-Methode in einem neuen Ausführungspfad auf.

run()

Ist ein normaler Methodenaufruf, wenn du ihn direkt aufrufst. Dadurch entsteht kein neuer Thread.

join()

Lässt den aufrufenden Thread warten, bis der andere Thread beendet ist oder die angegebene Wartezeit abläuft.

sleep()

Pausiert den aktuell laufenden Thread zeitlich. Die Methode ist kein Werkzeug zur zuverlässigen Koordination.

Keine Reihenfolge mit sleep() erzwingen

Eine feste Wartezeit beweist nicht, dass eine andere Aufgabe fertig ist. Verwende für Abhängigkeiten beispielsweise join(), Future.get(), Latches oder höherwertige Nebenläufigkeits-APIs.

Gemeinsamen Zustand sicher behandeln

Die Operation counter++ besteht aus Lesen, Erhöhen und Schreiben. Zwei Threads können denselben alten Wert lesen und anschließend beide denselben neuen Wert speichern. Ein Update geht dann verloren. Das Schlüsselwort volatile macht eine zusammengesetzte Operation nicht atomar.

import java.util.concurrent.atomic.AtomicInteger;

public final class RequestCounter {
    private final AtomicInteger value = new AtomicInteger();

    public int incrementAndGet() {
        return value.incrementAndGet();
    }

    public int currentValue() {
        return value.get();
    }
}

AtomicInteger passt zu einfachen atomaren Aktualisierungen. Sobald mehrere Werte gemeinsam eine Invariante bilden, brauchst du meist eine umfassendere Strategie.

  • Unveränderlichkeit: Geteilte Objekte können nach ihrer Konstruktion nicht mehr verändert werden.
  • Thread-Beschränkung: Veränderlicher Zustand gehört genau einem Thread.
  • synchronized oder Lock: Zusammengehörige Zugriffe werden als kritischer Abschnitt geschützt.
  • Concurrent Collections: Klassen wie ConcurrentHashMap stellen geeignete atomare Operationen bereit.

Aufgaben mit ExecutorService ausführen

Direkt erzeugte Threads sind für Lernbeispiele hilfreich. In Anwendungen sollte der Code, der eine Aufgabe beschreibt, aber häufig nicht selbst entscheiden, wie viele Threads entstehen. Ein ExecutorService trennt Einreichen, Ausführen und Beenden von Aufgaben.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorDemo {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
            Future result = executor.submit(() -> 21 * 2);

            System.out.println(result.get()); // 42
        }
    }
}

Runnable liefert kein Ergebnis. Callable darf dagegen einen Wert zurückgeben und eine Exception auslösen. Future.get() wartet auf das Ergebnis und macht einen Fehlschlag über eine ExecutionException sichtbar.

Ein ExecutorService ist AutoCloseable. Der try-with-resources-Block leitet deshalb ein geordnetes Herunterfahren ein. Bei langlebigen Diensten gehört die Lebensdauer des Executors zur Lebensdauer der Anwendungskomponente.

Virtual Threads sinnvoll einsetzen

Java kennt Plattform-Threads und Virtual Threads. Ein Plattform-Thread ist typischerweise eng an einen Betriebssystem-Thread gebunden. Ein Virtual Thread wird von der Java-Laufzeit verwaltet und kann beim blockierenden Warten von seinem sogenannten Carrier Thread gelöst werden.

Dadurch kann eine Anwendung sehr viele überwiegend wartende Aufgaben in einem einfachen Thread-pro-Aufgabe-Stil ausführen. Das erhöht bei passenden Server-Workloads den Durchsatz, nicht automatisch die Geschwindigkeit einer einzelnen Anfrage.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadDemo {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executor =
                     Executors.newVirtualThreadPerTaskExecutor()) {

            var first = executor.submit(() -> loadCustomer(17));
            var second = executor.submit(() -> loadOrders(17));

            System.out.println(first.get());
            System.out.println(second.get());
        }
    }

    private static String loadCustomer(int id) {
        return "Customer " + id; // Stellvertretend für blockierende I/O
    }

    private static String loadOrders(int customerId) {
        return "Orders for " + customerId;
    }
}
Plattform-Threads und Virtual Threads
Aspekt Plattform-Thread Virtual Thread
Verwaltung Typischerweise eng an einen Betriebssystem-Thread gebunden Von der Java-Laufzeit auf Carrier Threads geplant
Guter Einsatz CPU-Arbeit, wenige langlebige Worker, spezielle Thread-Eigenschaften Sehr viele überwiegend blockierende I/O-Aufgaben
Anzahl Begrenzte Ressource Sehr große Anzahl möglich
Priorität Konfigurierbar, aber selten eine gute Steuerungsstrategie Feste normale Priorität
Daemon-Status Daemon oder Non-Daemon Immer Daemon

Virtual Threads sind kein größerer Thread-Pool. Sie sollten normalerweise nicht gepoolt werden. Begrenze stattdessen ausdrücklich die knappe Zielressource, etwa über einen Semaphore für eine externe Schnittstelle.

Unterbrechung und kooperativer Abbruch

interrupt() beendet einen Thread nicht gewaltsam. Es setzt ein Unterbrechungssignal. Blockierende Methoden wie sleep(), join() oder viele Queue-Operationen reagieren darauf mit InterruptedException.

try {
    processNextItem();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

Kann die Methode die Exception nicht weitergeben, stellt sie den Interrupt-Status in der Regel wieder her. Dadurch können aufrufende Ebenen erkennen, dass ein Abbruch gewünscht ist. Die veralteten Methoden stop(), suspend() und resume() sind keine sichere Alternative.

Daemon Threads und Programmende

Die JVM beginnt mit dem Herunterfahren, wenn alle gestarteten Non-Daemon-Plattform-Threads beendet sind. Daemon-Threads sind deshalb nur für unterstützende Arbeit geeignet, die beim Programmende nicht zuverlässig abgeschlossen werden muss. Virtual Threads sind immer Daemon-Threads.

Keine wichtigen Daten nur einem Daemon-Thread anvertrauen

Ein Daemon-Thread garantiert nicht, dass ein Schreibvorgang, ein Log-Export oder eine Sicherung abgeschlossen wird. Für wichtige Abschlussarbeit brauchst du eine kontrollierte Lebensdauer und gegebenenfalls einen Shutdown-Hook.

Java und Kotlin: Threads und Coroutines

Kotlin läuft auf der JVM und kann dieselben Java-Thread- und Executor-APIs verwenden. Die Standardbibliothek bietet mit kotlin.concurrent.thread eine kompakte Hilfsfunktion. Häufiger setzen Kotlin-Anwendungen jedoch Coroutines ein.

Nebenläufigkeit in Java und Kotlin
Aspekt Java Kotlin
Aufgabe ohne Ergebnis Runnable Lambda beziehungsweise suspendierende Funktion
Thread-Erzeugung Thread.Builder oder Executor Java-API oder thread { ... }
Leichtgewichtige Nebenläufigkeit Virtual Threads Coroutines
Abbruch Kooperativ über Interrupts und API-Verträge Kooperativ über Coroutine-Cancellation

Eine Coroutine ist nicht einfach ein anderer Name für einen JVM-Thread. Coroutines werden durch eine Bibliothek geplant und können suspendieren, ohne einen Thread zu blockieren. Virtual Threads bleiben dagegen vollständige Thread-Objekte mit den bekannten Java-Regeln.

Best Practices und typische Anti-Patterns

Gute Leitlinien

  • Aufgabe und Ausführungsmechanismus trennen.
  • Executors kontrolliert schließen.
  • Threads und Aufgaben aussagekräftig benennen.
  • Geteilten veränderlichen Zustand minimieren.
  • Interrupts und Abbruchpfade testen.
  • Virtual Threads für passende I/O-Workloads einsetzen.

Warnsignale

  • run() wird statt start() aufgerufen.
  • sleep() soll eine Reihenfolge erzwingen.
  • Exceptions und Interrupts werden verschluckt.
  • Für jede kleine Aufgabe wird ungeplant ein Plattform-Thread erzeugt.
  • Gemeinsame Collections werden ohne Synchronisationskonzept verändert.
  • Virtual Threads sollen CPU-Arbeit beliebig beschleunigen.

Thread-Glossar

Thread
Ausführungspfad innerhalb eines Prozesses; in Java eine Instanz von Thread.
Plattform-Thread
Java-Thread, der typischerweise eng an einen Betriebssystem-Thread gebunden ist.
Virtual Thread
Leichtgewichtiger Java-Thread, den die Laufzeit auf einer kleineren Zahl von Carrier Threads plant.
Race Condition
Fehler, dessen Ergebnis von einer nicht zuverlässig kontrollierten zeitlichen Reihenfolge abhängt.
Executor
Komponente, die Aufgaben entgegennimmt und ihre Ausführung organisiert.
Interrupt
Kooperatives Signal, das einen Abbruch oder eine Unterbrechung anfordert.
Atomare Operation
Operation, die aus Sicht anderer Threads nicht teilweise beobachtet wird.

Fazit: Gute Nebenläufigkeit beginnt bei klaren Verantwortlichkeiten

Threads sind nicht nur eine Möglichkeit, Code gleichzeitig auszuführen. Sie verändern die Regeln für Sichtbarkeit, Reihenfolge, Fehlerbehandlung und Programmende. Deshalb sollte eine Anwendung Aufgaben von ihrer Ausführung trennen und gemeinsam veränderlichen Zustand bewusst begrenzen.

Die praktische Leitlinie: Beginne mit einem Runnable oder Callable, übergib die Aufgabe an einen passenden Executor und entscheide anhand des Workloads zwischen Plattform- und Virtual Threads. Erzeuge eigene Threads nur dann direkt, wenn ihre Lebensdauer und Verantwortung wirklich zur Anwendung passen.

Offizielle Quellen und weiterführende Informationen

Transparenzhinweis


Transparenzhinweis:
Die Inhalte auf tiny-tool.de werden sorgfältig recherchiert, redaktionell geprüft und regelmäßig aktualisiert. Quellen und Zitate werden möglichst nachvollziehbar angegeben. Dennoch übernehmen wir keine Garantie für Richtigkeit, Vollständigkeit oder Aktualität der bereitgestellten Informationen. Irrtümer sind nicht ausgeschlossen.

Redaktion und Einsatz von KI: Bei der Erstellung von Inhalten können digitale Werkzeuge – darunter auch KI-basierte Assistenzsysteme – unterstützend eingesetzt werden, etwa bei Recherche, Strukturierung, sprachlicher Überarbeitung, Übersetzung, Codeanalyse oder visueller Gestaltung. Veröffentlichte Inhalte werden redaktionell geprüft, bearbeitet und von Guido Zeuner freigegeben. Auswahl, Einordnung und Veröffentlichung liegen beim Menschen. KI-Ausgaben gelten nicht als eigenständige Quellen. KI-Systeme sind keine verantwortlichen Autoren oder Redakteure. Weitere Informationen zu Texten, Bildern, Videos und digitalen Personas findest du auf unserer Seite Transparenz beim Einsatz von Künstlicher Intelligenz.

Reichweitenmessung (VG WORT / METIS): Zur Ermittlung der Reichweite einzelner Texte können Zählmarken der VG WORT eingesetzt werden. Im Rahmen der METIS-Zugriffszählung kann eine Client-ID gebildet und ein sogenanntes „METIS Session Cookie“ gesetzt werden. Die Messung dient der statistischen Ermittlung von Textzugriffen und als Grundlage für mögliche Ausschüttungen der VG WORT. Nach Angaben der VG WORT werden dabei keine personenbezogenen Nutzungsprofile erstellt; die Messung dient nicht der Werbung oder dem Marketing-Tracking. Weitere Informationen findest du in unseren Datenschutzhinweisen.

Bitte beachte: Die Inhalte dienen ausschließlich der allgemeinen Information und stellen keine fachliche Beratung dar, insbesondere keine rechtliche, steuerliche, medizinische, technische oder finanzielle Beratung. Die Nutzung der Inhalte erfolgt auf eigene Verantwortung.

Werbung und Affiliate-Links: Einige Beiträge können werbliche Hinweise oder sogenannte Affiliate-Links enthalten. Diese werden entsprechend gekennzeichnet. Beim Klick entstehen dir keine zusätzlichen Kosten; wir erhalten gegebenenfalls eine kleine Provision.

Markenrechtlicher Hinweis: Alle Markennamen, Logos und Produktbezeichnungen sind Eigentum der jeweiligen Rechteinhaber und werden ausschließlich zur Identifikation und Beschreibung verwendet. Eine Verbindung zu den genannten Unternehmen besteht nur, wenn dies ausdrücklich angegeben wird.

Externe Links: Diese Website enthält Verweise auf externe Websites Dritter. Trotz sorgfältiger Prüfung übernehmen wir keine Verantwortung für deren Inhalte. Bei Bekanntwerden rechtswidriger Inhalte werden entsprechende Links geprüft und gegebenenfalls entfernt.