W3docs

Java Thread-Priorität

Java Thread-Prioritäten sind ein Hinweis, keine Garantie — was setPriority wirklich bewirkt, was nicht, und wann man es verwenden sollte.

Thread besitzt eine Priorität — eine ganze Zahl von 1 bis 10. Die JVM übergibt sie dem OS-Scheduler als Hinweis, welchen Thread die CPU als nächstes ausführen soll. Der Hinweis ist genau das — ein Hinweis. Das Betriebssystem kann ihn ignorieren, und auf den meisten Desktop- und Server-Betriebssystemen ist der Effekt irgendwo zwischen subtil und unsichtbar. Dieses Kapitel erklärt, wie die API aussieht, was tatsächlich darunter passiert, und die sehr engen Fälle, in denen das Setzen einer Priorität den Aufwand wert ist.

Die API

public final void setPriority(int newPriority);
public final int getPriority();

public static final int MIN_PRIORITY  = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY  = 10;

Drei Konstanten für die Lesbarkeit — der meiste Code, der mit Prioritäten arbeitet, verwendet diese anstelle von rohen Ganzzahlen:

Thread t = new Thread(this::housekeeping, "gc-poker");
t.setPriority(Thread.MIN_PRIORITY);                  // 1 — "background"
t.start();

Thread h = new Thread(this::handleRequest, "http");
h.setPriority(Thread.NORM_PRIORITY);                 // 5 — default
h.start();

Der Konstruktor verwendet die Priorität des übergeordneten Threads und die maximale Priorität der Thread-Gruppe des übergeordneten Threads — normalerweise 5. Das Setzen auf einen Wert außerhalb von 1..10 wirft eine IllegalArgumentException.

Was „Priorität" für die JVM bedeutet

Zwei Dinge, von denen keines „dieser Thread wird zuerst ausgeführt" bedeutet:

  1. Ein nativer Scheduling-Hinweis. Die JVM übersetzt die Zahl 1–10 in etwas, das das Host-Betriebssystem versteht. Unter Linux ist das ein nice-Wert über pthread_setschedparam; unter Windows ist es eine THREAD_PRIORITY_*-Konstante. Die Zuordnung ist implementierungsdefiniert in der JVM.
  2. Eine maximale Obergrenze pro Thread-Gruppe. Ein Thread kann nicht auf eine Priorität gesetzt werden, die höher ist als die maxPriority seiner Thread-Gruppe. Thread-Gruppen sind größtenteils veraltet, aber die Obergrenze gilt noch immer.

Was Priorität nicht bedeutet:

  • Es ist kein „Lock" — ein Thread mit hoher Priorität kann keinen Thread innerhalb eines kritischen Abschnitts verdrängen.
  • Es ist keine Garantie für die Reihenfolge. Zwei Threads mit Priorität 10 konkurrieren immer noch; einer von ihnen erhält die CPU zuerst.
  • Es beeinflusst nicht die Korrektheit. Jedes Programm, das „nur funktioniert", weil es auf Prioritäten angewiesen ist, hat einen echten Synchronisierungsfehler, der sich dahinter verbirgt.

Was das Betriebssystem tatsächlich tut

Verschiedene Host-Betriebssysteme behandeln den Hinweis unterschiedlich. Das aktuelle Verhalten, grob zusammengefasst:

  • Linux. Die Standard-JVM-Konfiguration behandelt Java-Prioritäten als nice-Werte. nice ist ein Hinweis an den CFS-Scheduler bezüglich des CPU-Anteils. Nicht-Root-Benutzer können die Priorität nur senken (positives nice); sie zu erhöhen (negatives nice) erfordert CAP_SYS_NICE, das die meisten JVMs nicht besitzen. In der Praxis ist setPriority(MAX_PRIORITY) aus einem Java-Prozess ohne Root-Rechte oft ein No-Op.
  • Windows. Prioritäten werden auf die sieben Windows-Thread-Prioritäten abgebildet. Die Abbildung ist aggressiver; Threads mit hoher Priorität erhalten tatsächlich mehr CPU. Aber man kann immer noch keinen Thread innerhalb eines Syscalls oder der ein Kernel-Lock hält, verdrängen.
  • macOS. Verwendet pthread_setschedparam wie Linux; ähnliches Verhalten.

Die Schlussfolgerung ist auf allen gleich: Prioritäten sind Empfehlungen, und unter Linux wird die Empfehlung oft komplett ignoriert. Verlasse dich nicht darauf.

Wann man setPriority tatsächlich verwenden sollte

Drei legitime Verwendungszwecke, in der Reihenfolge, wie oft sie vorkommen:

  1. Einen Hintergrund-Thread auf MIN_PRIORITY setzen, damit das Betriebssystem ihn unter Last sanft deprioritisiert. Ein Telemetrie-Uploader, ein Log-Flusher, ein Cache-Rebuild-Job — keiner dieser sollte jemals benutzerseitige Latenz verursachen. Sie auf 1 zu setzen ist ein kostenloser Hinweis, der dem Betriebssystem sagt: „Dieser kann ruhig ein wenig ausgehungert werden."
  2. Einen Echtzeit-Helfer auf MAX_PRIORITY setzen in einer JVM, der das Privileg dazu gewährt wurde. Selten außerhalb von Audio, Game-Loops oder spezialisierten Low-Latency-Systemen.
  3. Nichts tun. Der Standard von NORM_PRIORITY ist die richtige Antwort für fast jeden Thread, den man jemals erstellen wird.

Was man stattdessen verwenden sollte

Wenn der Grund, aus dem man über Prioritäten nachdenkt, einer dieser ist — gibt es ein besseres Werkzeug:

ZielStattdessen verwenden
„Lange Jobs sollten kurze Jobs nicht blockieren"Zwei ExecutorServices — ein kleiner für latenzempfindliche Arbeit, ein größerer für Batch-Aufgaben
„Thread A muss vor Thread B ausgeführt werden"join, ein CountDownLatch oder eine CompletableFuture-Kette
„Nur ein Thread zur gleichen Zeit in dieser Methode"synchronized oder ein ReentrantLock
„Aushungern von niedrig priorisierten Aufgaben vermeiden"Eine faire Warteschlange (new LinkedBlockingQueue<>() mit einem Lotsystem oder ein PriorityBlockingQueue mit expliziter Aufgabenpriorität — nicht Thread-Priorität)

Das Muster: Priorität ist ein Scheduling-Hinweis zwischen laufbereiten Threads. Das richtige Werkzeug für „welcher Thread zuerst läuft" ist fast immer Synchronisierung, nicht Priorität.

Prioritätsumkehr

Das klassische Fehlermuster von prioritätsbasiertem Scheduling:

  • Ein Thread L mit niedriger Priorität erwirbt Lock M.
  • Ein Thread H mit hoher Priorität versucht M zu erwerben und blockiert, weil er auf L wartet.
  • Ein Thread Med mit mittlerer Priorität führt CPU-gebundene Arbeit aus und verhindert, dass L eingeplant wird.
  • H ist effektiv auf der Priorität von Med — umgekehrt.

Java löst das nicht für einen. Das nice-System des Kernels auch nicht. Die Lösung ist entweder (a) sich nicht auf Prioritäten für Korrektheit zu verlassen, oder (b) ReentrantLock mit einer Fair-Ordering-Richtlinie und kurzen kritischen Abschnitten zu verwenden, damit das Umkehrungsfenster begrenzt ist. Wir werden Fairness im Kapitel ReentrantLock kennenlernen.

Thread-Gruppen (größtenteils historisch)

ThreadGroup gp = new ThreadGroup("workers");
gp.setMaxPriority(7);
Thread t = new Thread(gp, runnable, "worker");
t.setPriority(10);                                   // capped to 7 by the group

Thread-Gruppen stammen aus Java 1.0 und waren ursprünglich das „Control Panel" für Batches von Threads. Sie wurden fast vollständig durch ExecutorService ersetzt, und die meisten ihrer Methoden sind veraltet. Man sieht ThreadGroup in Stack-Traces und Dumps; typischerweise erstellt man keine selbst. Das einzig noch aktive Element ist die maxPriority-Obergrenze pro Gruppe.

Ein praktisches Beispiel: Prioritäten in Aktion beobachten

Das folgende Programm startet mehrere CPU-gebundene Threads mit unterschiedlichen Prioritäten und misst, wie viel Arbeit jeder in einem festen Zeitfenster erledigt hat. Unter Linux werden alle wahrscheinlich ähnliche Arbeitsmengen erledigen — das ist der Punkt. Unter Windows kann man einen bedeutenden Unterschied sehen. So oder so ist die Lektion dieselbe.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Alle drei Threads haben höchstwahrscheinlich ähnliche Arbeitsmengen erledigt, obwohl einer mit Priorität 1 und ein anderer mit Priorität 10 lief. Auf einer Linux JVM, die als normaler Benutzer läuft, kann die JVM die Priorität nicht wirklich über den Standard anheben — der Aufruf setPriority(10) hat das Feld gesetzt, aber das Betriebssystem hat ihn als normale Priorität behandelt. Das Java-Feld ändert sich; das tatsächliche Scheduling kaum.
  • Der getPriority()-Aufruf spiegelte den gesetzten Wert wider, unabhängig davon, ob das Betriebssystem ihn berücksichtigt hat. Das ist wichtig beim Debuggen: das Feld zeigt, was der Code angefordert hat, nicht was das Betriebssystem tat.
  • Die CPU-Schleife hat absichtlich nie geblockt (kein Thread.sleep, kein wait, kein I/O). Das ist das einzige Szenario, in dem Priorität plausibel eine Rolle spielen kann — reine CPU-Konkurrenz. Sobald ein Thread blockiert, wird die Priorität irrelevant; der blockierte Thread befindet sich ohnehin nicht auf einer CPU.
  • setPriority(11) hat IllegalArgumentException geworfen. Die Grenzen werden auf der Java-Seite durchgesetzt. Priorität 0 wirft ebenfalls; der gültige Bereich ist genau 1 bis 10.
  • Die richtige Schlussfolgerung aus dem kleinen (oder nicht vorhandenen) Unterschied in der Batch-Anzahl ist: wenn das Design anfängt, so zu wirken, als ob es Prioritäten für die Korrektheit benötigt, sollte man es durch explizite Synchronisierung ersetzen. Zwei ExecutorServices verwenden, wenn man Isolation möchte; Locks verwenden, wenn man Reihenfolge möchte; Warteschlangen verwenden, wenn man Fairness möchte. Priorität ist ein letztes Mittel, und unter Linux ist es kaum ein Mittel überhaupt.

Was als nächstes kommt

Das nächste Kapitel, Java Synchronization, beginnt die eigentliche Geschichte von sicherem nebenläufigem Code — das synchronized-Schlüsselwort, intrinsische Monitore und wie Javas erstklassiges Lock-Primitiv funktioniert.

Übungen

Übung
Sie arbeiten unter Linux und führen ein Java-Programm als normaler Benutzer aus. Sie rufen `t.setPriority(Thread.MAX_PRIORITY)` für einen Worker auf. Was passiert tatsächlich mit dem OS-Level-Scheduling dieses Threads?
Sie arbeiten unter Linux und führen ein Java-Programm als normaler Benutzer aus. Sie rufen `t.setPriority(Thread.MAX_PRIORITY)` für einen Worker auf. Was passiert tatsächlich mit dem OS-Level-Scheduling dieses Threads?
Was this page helpful?