W3docs

Java Thread-Lebenszyklus

Die Zustände eines Java-Threads — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — und wie sie sich ändern.

Ein Java-Thread hat nicht viele Zustände — sechs, alle als Werte des Thread.State-Enums. Aber diese sechs sind das Vokabular von Thread-Dumps, Profilern und jeder Untersuchung der Frage „warum hängt mein Programm". Zu wissen, was jeder Zustand bedeutet und welche Übergänge möglich sind, verwandelt einen Thread-Dump von einer Wand aus Stack-Traces in eine Diagnose.

Die sechs Zustände

// java.lang.Thread.State — the six possible thread states
public enum State {
  NEW,                  // created, never started
  RUNNABLE,             // started; running or ready to run on a CPU
  BLOCKED,              // waiting for a monitor lock to enter a synchronized block
  WAITING,              // parked indefinitely (Object.wait, Thread.join, LockSupport.park)
  TIMED_WAITING,        // parked with a timeout (sleep, wait(ms), join(ms), park(nanos))
  TERMINATED            // run() has returned
}

Die zwischen ihnen erlaubten Übergänge bilden einen einfachen Lebenszyklus:

   NEW
    |  start()
    v
RUNNABLE  <----------+--------+-------+
    |   |            |        |       |
    |   | enters     | wakes  | timeout
    |   v sync       | from   | expires
    |  BLOCKED       | wait/  |
    |   |            | join   |
    |   | acquires   |        |
    |   v lock       |        |
    +-> RUNNABLE     |        |
    |                |        |
    | wait/join/park |        |
    v                |        |
WAITING -------------+        |
    |                         |
    | wait(ms)/join(ms)/sleep |
    v                         |
TIMED_WAITING ----------------+
    |
    | run() returns
    v
TERMINATED

Jeder Zustand entspricht etwas, das in einem Thread-Dump sichtbar ist. Gehen wir sie durch.

NEW

Ein Thread, den Sie konstruiert, aber noch nie start() aufgerufen haben. Es wurden keine OS-Ressourcen zugewiesen; nichts läuft. Die einzigen Übergänge heraus sind:

  • start()RUNNABLE
  • Der Thread wird garbage-collected, ohne je zu laufen

Sie können start() genau einmal aufrufen. Ein zweiter Aufruf wirft IllegalThreadStateException.

RUNNABLE

„Der Thread ist am Leben und läuft gerade entweder auf einer CPU oder ist bereit zu laufen." Java fasst das OS-eigene „running" und „runnable" in einem einzigen Zustand zusammen — es gibt keine Möglichkeit, allein aus Thread.State zu erkennen, ob der Thread gerade CPU verbraucht. Der OS-Scheduler entscheidet, welche RUNNABLE-Threads tatsächlich einen Kern erhalten.

Ein RUNNABLE-Thread ist auch der Zustand, in dem sich ein Thread befindet, wenn er auf I/O wartet (InputStream.read, Socket.read, FileChannel.read). Das überrascht viele: Der Thread ist nur in dem Sinne „bereit zu laufen", dass ihn nichts in der JVM aufhält. Das OS weiß, dass der Thread auf die Festplatte wartet; die JVM weiß es nicht und meldet daher RUNNABLE. Wenn Sie in einem Thread-Dump einen Thread als RUNNABLE sehen, dessen oberster Frame socketRead0 oder Ähnliches ist, ist der Thread durch einen Syscall blockiert — er verbrennt keine CPU.

Warnung

RUNNABLE bedeutet nicht „beschäftigt". Es ist der am häufigsten missverstandene Zustand in einem Thread-Dump: Ein Thread, der in einem blockierenden I/O-Aufruf steckt (socketRead0, FileInputStream.read), meldet RUNNABLE, obwohl er null CPU verwendet. Schließen Sie nicht von seinem Zustand darauf, dass ein Thread heiß ist — lesen Sie seinen obersten Stack-Frame oder samplen Sie mit einem Profiler.

BLOCKED

Der Thread steht vor der Tür eines synchronized-Blocks und wartet auf den Monitor-Lock. Ein anderer Thread hält ihn; dieser hat sich in die Warteschlange eingereiht. Sobald der Halter freigibt, gewinnt einer der Wartenden den Lock und wechselt zurück zu RUNNABLE.

BLOCKED ist spezifisch für synchronized — den intrinsischen Lock-Mechanismus, den die JVM einbaut. Code, der auf einen ReentrantLock wartet, zeigt nicht BLOCKED; er zeigt WAITING (weil ReentrantLock auf LockSupport.park aufgebaut ist). Das ist ein kleiner, aber wichtiger Unterschied, wenn Sie Dumps lesen.

Die klassische Thread-Dump-Signatur für BLOCKED:

"worker-3" #19 prio=5 ... waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
   at com.acme.Cache.put(Cache.java:42)
   - waiting to lock <0x000000076ab8e220> (a java.util.HashMap)
   at com.acme.Cache.miss(Cache.java:67)

Zwei Informationen: auf welchen Monitor Sie warten (<0x000000076ab8e220>) und welche Methode vor der Tür steht. Suchen Sie im selben Dump nach - locked <0x000000076ab8e220> und Sie haben den Thread gefunden, der ihn hält.

WAITING

Der Thread hat sich entschieden, unbegrenzt zu warten. Drei Dinge versetzen einen Thread hierhin:

  • Object.wait() — gibt den Monitor frei und parkt, bis jemand notify/notifyAll aufruft.
  • Thread.join() — ohne Timeout, parkt, bis der Ziel-Thread beendet ist.
  • LockSupport.park() — das Primitiv, auf dem ReentrantLock.lock(), await(), BlockingQueue.take() und das gesamte java.util.concurrent aufgebaut sind.

Ein WAITING-Thread verbraucht im Wesentlichen keine Ressourcen außer seinem Stack. Er wird nicht übergehen, bis jemand anderes etwas tut — ein notify, ein abgeschlossener Join-Ziel-Thread, ein LockSupport.unpark. Wenn ihn nichts jemals aufweckt, bleibt er dort für immer. So sehen stille Deadlocks in einem Dump aus: zwei Threads, beide WAITING, beide haltend, was der andere braucht.

TIMED_WAITING

Gleiche Idee wie WAITING, aber mit einer Deadline. Der Thread wacht von selbst auf, wenn der Timeout abläuft, auch wenn sonst nichts passiert. Dinge, die TIMED_WAITING erzeugen:

  • Thread.sleep(ms)
  • Object.wait(ms)
  • Thread.join(ms)
  • LockSupport.parkNanos(...), LockSupport.parkUntil(...)
  • BlockingQueue.poll(timeout, unit), Future.get(timeout, unit), usw.

Wenn ein Thread für die von Ihnen angegebene Dauer zuverlässig in TIMED_WAITING steckt, ist das kein Fehler. Wenn er über den Timeout hinaus dort bleibt, wurde er neu geparkt — jemand hat wait(1000) in einer Schleife aufgerufen oder die Queue ist immer noch leer.

TERMINATED

run() ist zurückgekehrt (normal oder durch eine Ausnahme). Der Thread ist fertig; er kann nicht neu gestartet werden. t.isAlive() gibt false zurück. Sie können seinen Namen und seine ID noch für Log-/Debug-Zwecke lesen, aber der Thread selbst ist beendet.

Zustand aus dem eigenen Code lesen

Thread.State ist öffentlich abfragbar, aber der Wert ist ein Snapshot — er kann sich zwischen dem Aufruf und Ihrer Verwendung ändern. In der Produktion verzweigen Sie so gut wie nie danach; Sie verwenden ihn für Logging und Diagnose. Die JVM stellt auch ThreadMXBean für vollständige Thread-Dumps bereit, was die meisten JMX-Dashboards anzeigen.

Thread t = new Thread(() -> doWork(), "worker");
System.out.println(t.getState());          // NEW
t.start();
System.out.println(t.getState());          // RUNNABLE (or TIMED_WAITING/BLOCKED/etc., racy)
t.join();
System.out.println(t.getState());          // TERMINATED

Ein ausgearbeitetes Beispiel: jeden Zustand beobachten

Das folgende Programm erstellt Threads, die jeweils in einem anderen Zustand feststecken, und gibt dann aus, in welchem Zustand sie sich befinden.

java— editable, runs on the server

Was aus dem Lauf mitgenommen werden sollte:

  • Jeder der sechs Zustände war durch Code im selben Programm erreichbar. NEW und TERMINATED sind die Grenzfälle; die mittleren vier (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING) sind die, die Sie in einem echten Thread-Dump sehen werden.
  • Der blocked-thread meldete BLOCKED, weil der Halter den synchronized-Monitor besaß. Hätten wir stattdessen einen ReentrantLock verwendet, hätte derselbe Code-Pfad WAITING gemeldet (da Lock.lock() via LockSupport parkt). Der Zustandsname sagt Ihnen welche Art von Warten, nicht nur „dieser Thread steckt fest."
  • Der waiting-thread wäre für immer in WAITING geblieben, wenn main nicht cond.notify() aufgerufen hätte. Der WAITING-Zustand hat keinen Timeout — jemand anderes muss ihn aufwecken. Genau so erzeugt ein verpasstes notify einen Deadlock, den keine Ausnahme je meldet.
  • Der CPU-brennende Thread meldete RUNNABLE, egal ob er tatsächlich auf einem Kern lief oder nur in der Run-Queue auf einen wartete. Die JVM unterscheidet nicht „running" von „ready"; das OS tut es. Wenn Sie wissen müssen, welche Threads tatsächlich CPU verbrauchen, profilieren Sie mit einem Sampling-Profiler — getState() wird es Ihnen nicht sagen.
  • Nachdem tRunning.join() zurückgekehrt war, war sein Zustand TERMINATED. Sie können noch seinen Namen, seine ID und sein State-Objekt abfragen, aber der Thread ist weg — isAlive() ist false und start() würde werfen. Threads sind Einwegprodukte: Wenn einer endet, erstellen Sie einen neuen. (Das ist die Hauptmotivation für das Executor-Framework — ein ExecutorService verwendet denselben OS-Thread für viele Aufgaben wieder.)

Was kommt als Nächstes

Das nächste Kapitel, Java Thread-Methoden, geht die Methodenoberfläche von Thread durch — sleep, join, yield, interrupt, holdsLock und die statischen Hilfsmethoden — mit den Fallstricken für jede einzelne.

Übungen

Übung
Ein Thread befindet sich im Zustand `BLOCKED`. Worauf wartet er?
Ein Thread befindet sich im Zustand `BLOCKED`. Worauf wartet er?
Übung
Ein Thread-Dump zeigt einen Thread als `RUNNABLE` mit `socketRead0` oben im Stack. Was tut er tatsächlich?
Ein Thread-Dump zeigt einen Thread als `RUNNABLE` mit `socketRead0` oben im Stack. Was tut er tatsächlich?
Übung
Welcher Aufruf versetzt einen Thread in `TIMED_WAITING` statt in `WAITING`?
Welcher Aufruf versetzt einen Thread in `TIMED_WAITING` statt in `WAITING`?
Was this page helpful?