Java Thread-Methoden
Wichtige Java Thread-Methoden — start, run, sleep, join, interrupt, yield, setDaemon — mit typischen Fallen in echtem Code erklärt.
Thread bietet viele Methoden, aber nur eine Handvoll davon ruft man in echtem Code tatsächlich auf. Dieses Kapitel geht diese Handvoll durch — was jede einzelne tut, was sie nicht tut, und welche Fehler korrekt aussehen, bis sie es nicht mehr sind. Die früheren Kapitel haben diese Methoden nur kurz erwähnt; hier werden sie einzeln festgehalten.
Diese Seite behandelt Thread-Lebenszyklus und -Steuerung: start vs. run, sleep, join, das kooperative Abbruchprotokoll mit interrupt, sowie die kleineren Hilfsmethoden (yield, currentThread, Benennung, Daemon-Status, Priorität, holdsLock, onSpinWait). Einen Gesamtüberblick darüber, wie ein Thread die Zustände NEW, RUNNABLE, BLOCKED, WAITING und TERMINATED durchläuft, findet man unter Java Thread Life Cycle.
start() vs. run()
Der häufigste Multithreading-Bug:
Thread t = new Thread(() -> work(), "worker");
t.run(); // wrong: runs work() on the CURRENT thread
t.start(); // right: spawns a new OS thread, returns immediatelystart() ist die einzige Methode, die einen neuen OS-Thread erzeugt. run() ist der Rumpf der Arbeit — ihn direkt aufzurufen ist nur ein normaler Methodenaufruf, der zurückkehrt, wenn die Arbeit abgeschlossen ist. Wenn kein start() zu sehen ist, findet keine Parallelität statt.
start() ist außerdem nur einmal verwendbar. Nachdem run() zurückgekehrt ist, befindet sich der Thread im Zustand TERMINATED und kann nicht neu gestartet werden. Ein zweiter Aufruf von start() wirft eine IllegalThreadStateException.
Thread.sleep(ms)
Der statische Aufruf, der den aktuellen Thread für mindestens die angegebene Dauer anhält:
Thread.sleep(1500); // sleep 1.5 seconds
Thread.sleep(0, 250); // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500)); // Java 19+ overloadDrei wichtige Punkte:
- Er wirft
InterruptedException. Sleep ist unterbrechbar — so wird einem Worker mitgeteilt, dass er aufhören soll zu schlafen und sich beenden soll. Man entweder gibt die Exception weiter (mitthrowsdeklarieren) oder fängt sie ab und setzt das Flag mitThread.currentThread().interrupt()neu. - Er gibt keine Locks frei. Ein schlafender Thread hält jeden Lock, den er vorher gehalten hat. Ruft man
Thread.sleepinnerhalb einessynchronized-Blocks auf, betritt kein anderer Thread den Block, solange man schläft. Das ist fast immer ein Bug; verwendewaitoderCondition.await(siehe Inter-thread Communication), wenn man den Lock freigeben muss. - Das Timing ist "mindestens", nicht "genau". Das OS könnte einen unter Last etwas zu spät aufwecken; früher wird man nie geweckt.
t.join() und t.join(ms)
Warten, bis ein anderer Thread beendet ist:
t.join(); // block until t terminates
t.join(2000); // block up to 2 seconds, then continue regardless
boolean done = t.join(Duration.ofSeconds(2));// Java 19+, returns whether it finishedjoin ist die Art, wie man mehrstufige parallele Arbeit zusammensetzt: ein paar Threads starten, laufen lassen, alle mit join zusammenführen, Ergebnisse lesen. join() kehrt zurück, wenn das run() des Ziel-Threads zurückgekehrt ist (ob normal oder durch Exception). Es wirft ebenfalls InterruptedException, sodass Aufrufer aus dem Warten unterbrochen werden können.
Ein subtiler Punkt: join(0) bedeutet "join ohne Timeout" (d. h. ewig warten), nicht "join mit Null-Timeout". Wenn man wirklich "sofort aufgeben" möchte, verwendet man stattdessen t.isAlive().
t.interrupt() und das Flag
Das kooperative Abbruchprotokoll in drei Aufrufen:
t.interrupt(); // set t's interrupt flag (and unblock sleep/wait/join/park)
t.isInterrupted(); // ask whether the flag is set (does NOT clear)
Thread.interrupted(); // static; ask current thread, and CLEAR the flagDas Flag ist nur ein volatile boolean auf dem Thread-Objekt. interrupt() setzt es. Befindet sich t gerade in sleep, wait, join oder LockSupport.park (oder vielen blockierenden java.nio-Aufrufen), wirft dieser blockierende Aufruf sofort eine InterruptedException. Andernfalls wartet das Flag darauf, dass der Worker es selbst bemerkt.
Ein Worker, der unterbrechbar sein möchte, hat zwei Verantwortlichkeiten:
Thread.currentThread().isInterrupted()zwischen lang laufenden Schritten prüfen.- In jedem
catch (InterruptedException e)entweder die Exception weitergeben oder das Flag mitThread.currentThread().interrupt()neu setzen — es niemals still schlucken.
while (!Thread.currentThread().isInterrupted()) {
try {
doOneUnit();
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag for the loop check
}
}Thread.yield() — fast nie das richtige Mittel
Thread.yield(); // hint: please run someone elseEin unverbindlicher Hinweis an den Scheduler. Das OS kann ihn ignorieren. Es gibt im Wesentlichen keinen Produktionscode, der yield benötigt — wenn man auf ein Ereignis warten will, verwendet man wait, eine Condition oder ein Semaphore. yield greift man nur für Mikro-Benchmarks, Deadlock-Test-Harnesses oder beim Schreiben der JVM selbst heraus.
Thread.currentThread()
Der statische Zugriff auf den Thread, auf dem der aufrufende Code läuft. Die zwei Verwendungsweisen, die man sieht:
String who = Thread.currentThread().getName();
Thread.currentThread().interrupt(); // re-arm the flag after catching InterruptedExceptiongetName() ist auch der Standardweg, Log-Zeilen zu beschriften, damit man Threads in der Produktionsausgabe unterscheiden kann.
getName / setName
Namen sind wichtig für das Debugging. Der Standardname (Thread-3) ist in einem Thread-Dump nutzlos.
Thread t = new Thread(this::flush, "flush-loop"); // name at construction (preferred)
t.setName("flush-loop-2"); // rename later if a role changesMan kann jederzeit umbenennen, aber der Wert zum Zeitpunkt des Dumps oder Logs ist das, was der Leser sieht. Immer einen Namen an den Konstruktor übergeben.
setDaemon(true)
Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true); // BEFORE start(); else IllegalThreadStateException
t.start();Daemon-Threads halten die JVM nicht am Leben — wenn der letzte Nicht-Daemon-Thread beendet wird, reißt die JVM die Daemons einfach heraus. Man verwendet sie für Haushaltsarbeiten, die mit dem Programm sterben sollen (Timer, Metrik-Flusher, Polling-Schleifen). Man verwendet sie nicht für Arbeit, deren Abschluss man tatsächlich braucht.
setPriority(int)
t.setPriority(Thread.MAX_PRIORITY); // 10
t.setPriority(Thread.MIN_PRIORITY); // 1
t.setPriority(Thread.NORM_PRIORITY); // 5 (default)Größtenteils beratend. Das nächste Kapitel behandelt Prioritäten im Detail; vorerst das Wichtigste: Man verlässt sich nicht auf sie für Korrektheit, das OS entscheidet, was sie bedeuten.
Thread.holdsLock(obj)
Eine statische Debug-Hilfsmethode:
assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";Gibt true zurück, wenn der aufrufende Thread den intrinsischen Monitor von obj hält. Nützlich, um zu behaupten "diese Methode darf nur von innerhalb eines synchronized-Blocks aufgerufen werden", ohne auf dem Happy-Path Lock-Akquisitionskosten zu zahlen.
Thread.onSpinWait() — Java 9+
while (!done) {
Thread.onSpinWait(); // hint to the CPU: I'm spinning, slow down
}Ein Hinweis auf CPU-Ebene, der Pipelines pausiert und den Stromverbrauch während einer engen Spin-Schleife reduziert. Es ist speziell für den sehr engen Fall gedacht, in dem man einige Mikrosekunden dreht und darauf wartet, dass ein anderer Thread ein Flag kippt; es ist kein allgemeines "CPU aufgeben"-Signal. Für alles Längere verwendet man LockSupport.park oder eine Condition.
Ein praktisches Beispiel: die meisten davon an einem Ort
Das Programm unten verwendet start, join mit Timeout, interrupt, sleep, isInterrupted und setName zusammen — die Methoden, die man in der Produktion tatsächlich aufrufen würde.
Was man aus dem Ablauf mitnehmen kann:
- Die Zeile
bad.run()hatran on: mainausgegeben. Es wurde kein neuer Thread erstellt.bad.isAlive()war danachfalse, weilstart()nie aufgerufen wurde. Jedes Multithreading-Programm hat diesen Bug irgendwann; wenn man ihn einmal gemacht hat, macht man ihn nie wieder. - Das
slow.join(300)ist nach etwa 300 ms zurückgekehrt, obwohlslow2000 ms geschlafen hätte.isAlive()war nochtrue.join(ms)ist das zeitbegrenzte Warten — nützlich, wenn man einem Worker eine faire Chance geben will, fertig zu werden, bevor man eskaliert. slow.interrupt()hat seinenThread.sleepsofort durch Werfen einerInterruptedExceptionim Worker beendet. Das ist der Vertrag: unterbrechbare blockierende Aufrufe reagieren aufinterrupt(), indem sie mit der Exception aussteigen — so funktioniert kooperatives Abbrechen in der Praxis.- Der
bookkeeper-Worker hatInterruptedExceptiongefangen und das Flag mitThread.currentThread().interrupt()neu gesetzt. Das nachfolgendeisInterrupted()hattruezurückgegeben. Ohne dieses Neusetzen geht das Flag verloren und jeder Code weiter oben im Call-Stack denkt, es sei nie ein Interrupt passiert. daemon.setDaemon(true)wurde vorstart()aufgerufen — danach hätte es eineIllegalThreadStateExceptiongeworfen. Und alsmainzurückkehrte, wurde der Daemon mitten im Schlaf beendet; die JVM ist beendet, weil kein Nicht-Daemon-Thread mehr vorhanden war. Das ist der Daemon-Kompromiss: blockiert niemals den JVM-Exit, hat niemals eine Garantie auf Fertigstellung.
Was kommt als Nächstes
Das nächste Kapitel, Java Thread Priority, behandelt die setPriority-Methode auf Thread, was die Prioritäten auf echten Betriebssystemen tatsächlich tun, und warum man sie eher als Hinweis denn als Garantie behandeln sollte.