Die Vorhersage der Thread-Ausführungsreihenfolge in der JVM
Einführung in Threading
Threading bezieht sich auf die Praxis, Programmprozesse gleichzeitig auszuführen, um die Leistung von Anwendungen zu verbessern. Obwohl es in Geschäftsanwendungen nicht üblich ist, direkt mit Threads zu arbeiten, werden sie in Java-Frameworks ständig verwendet. Ein Beispiel sind Frameworks, die eine große Menge an Informationen verarbeiten, wie Spring Batch, die Threads zur Datenverwaltung nutzen. Die gleichzeitige Manipulation von Threads oder CPU-Prozessen verbessert die Leistung und führt zu schnelleren, effizienteren Programmen.
Der Haupt-Thread in Java: Die main()-Methode
Auch wenn Entwickler noch nie direkt mit Java-Threads gearbeitet haben, haben sie indirekt damit gearbeitet. Die main()-Methode von Java enthält nämlich einen Haupt-Thread. Jedes Mal, wenn die main()
-Methode ausgeführt wird, läuft auch der Haupt-Thread
.
Der Lebenszyklus eines Java-Threads
Beim Arbeiten mit Threads ist es wichtig, sich der verschiedenen Zustände eines Threads bewusst zu sein. Der Lebenszyklus eines Java-Threads umfasst sechs Zustände:
- Neu: Ein neuer
Thread()
wurde instanziiert. - Runnable: Die
start()
-Methode desThread
wurde aufgerufen. - Running: Die
start()
-Methode wurde aufgerufen und der Thread läuft. - Suspended: Der Thread ist vorübergehend angehalten und kann von einem anderen Thread fortgesetzt werden.
- Blocked: Der Thread wartet auf eine Gelegenheit zur Ausführung. Dies geschieht, wenn ein Thread bereits die
synchronized()
-Methode aufgerufen hat und der nächste Thread warten muss, bis dieser abgeschlossen ist. - Beendet: Die Ausführung des Threads ist abgeschlossen.
Gleichzeitige Verarbeitung: Eine Thread-Klasse erweitern
Man kann die gleichzeitige Verarbeitung am einfachsten erreichen, indem eine Thread
-Klasse erweitert wird. Hier ist ein Beispiel:
public class CustomThread extends Thread {
CustomThread(String threadName) {
super(threadName);
}
public static void main(String... args) {
System.out.println(Thread.currentThread().getName() + " läuft");
new CustomThread("customThread").start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " läuft");
}
}
In diesem Beispiel laufen zwei Threads: der MainThread
und der CustomThread
. Wenn die start()
-Methode mit dem neuen customThread()
aufgerufen wird, läuft die Logik in der run()
-Methode.
Verwendung des Runnable-Interfaces
Anstatt Vererbung zu verwenden, können Entwickler das Runnable-Interface implementieren. Das Übergeben von Runnable
innerhalb eines Thread
-Konstruktors führt zu weniger Kopplung und mehr Flexibilität.
public class RunnableExample implements Runnable {
public static void main(String... args) {
System.out.println(Thread.currentThread().getName());
new Thread(new RunnableExample()).start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
Non-Daemon vs Daemon-Threads
In Bezug auf die Ausführung gibt es zwei Arten von Threads:
- Non-Daemon-Threads werden bis zum Ende ausgeführt. Der Haupt-Thread ist ein gutes Beispiel für einen Non-Daemon-Thread.
- Ein Daemon-Thread ist das Gegenteil, ein Prozess, der nicht bis zum Ende ausgeführt werden muss.
Es ist wichtig zu beachten, dass, wenn ein umschließender Non-Daemon-Thread endet, bevor ein Daemon-Thread endet, wird der Daemon-Thread nicht bis zum Ende ausgeführt.
Thread-Priorität und die JVM
Man kann die Ausführungspriorität von Threads mit der setPriority
-Methode festlegen, aber wie dies gehandhabt wird, hängt von der JVM-Implementierung ab. Linux, MacOS und Windows haben unterschiedliche JVM-Implementierungen, und jede behandelt die Thread-Priorität gemäß ihren eigenen Vorgaben.
public class ThreadPriorityExample {
public static void main(String... args) {
Thread aliceThread = new Thread(() -> System.out.println("Alice"));
Thread bobThread = new Thread(() -> System.out.println("Bob"));
Thread charlieThread = new Thread(() -> System.out.println("Charlie"));
aliceThread.setPriority(Thread.MAX_PRIORITY);
bobThread.setPriority(Thread.NORM_PRIORITY);
charlieThread.setPriority(Thread.MIN_PRIORITY);
charlieThread.start();
bobThread.start();
aliceThread.start();
}
}
Selbst wenn aliceThread
als MAX_PRIORITY
festgelegt wird, gibt es keine Garantie, dass dieser Thread zuerst ausgeführt wird. Stattdessen wird die Reihenfolge der Ausführung zufällig sein.
Herausforderung: Java-Threads
Folgendes Beispiel stellt eine Herausforderung für Java-Threads dar:
public class ThreadChallenge {
private static int adrenalineLevel = 10;
public static void main(String... args) {
new Vehicle("Car").start();
Vehicle fastVehicle = new Vehicle("Motorbike");
fastVehicle.setPriority(Thread.MAX_PRIORITY);
fastVehicle.setDaemon(false);
fastVehicle.start();
Vehicle slowVehicle = new Vehicle("Bicycle");
slowVehicle.setPriority(Thread.MIN_PRIORITY);
slowVehicle.start();
}
static class Vehicle extends Thread {
Vehicle(String vehicleName) { super(vehicleName); }
@Override public void run() {
adrenalineLevel++;
if (adrenalineLevel == 13) {
System.out.println(this.getName());
}
}
}
}
Die Frage lautet: Was wird die Ausgabe dieses Codes sein?
A. Car
B. Motorbike
C. Bicycle
D. Unbestimmt
Im obigen Code wurden drei Threads erstellt. Der erste Thread ist Car
, der mit der Standardpriorität zugewiesen wurde. Der zweite Thread ist Motorbike
, der MAX_PRIORITY
zugewiesen wurde. Der dritte ist Bicycle
, mit MIN_PRIORITY
. Dann wurden die Threads gestartet.
Um die Reihenfolge zu bestimmen, in der die Threads ausgeführt werden, sollte beachtet werden, dass die Vehicle
-Klasse die Thread
-Klasse erweitert und dass der Thread-Name im Konstruktor übergeben wurde. Die run()
-Methode wurde mit einer Bedingung überschrieben: adrenalineLevel == 13
.
Obwohl Bicycle
der dritte Thread in der Ausführungsreihenfolge ist und MIN_PRIORITY
hat, gibt es keine Garantie, dass er zuletzt ausgeführt wird.
Zusammenfassend wird das Ergebnis D: Unbestimmt sein, da es keine Garantie dafür gibt, dass der Thread-Planer der Ausführungsreihenfolge oder der Thread-Priorität folgt.
Häufige Fehler bei Java-Threads
- Die
run()
-Methode aufzurufen, um einen neuen Thread zu starten. - Zu versuchen, einen Thread zweimal zu starten (dies führt zu einer
IllegalThreadStateException
). - Mehrere Prozesse zuzulassen, die den Zustand eines Objekts ändern, wenn dies nicht geschehen sollte.
- Programmlogik zu schreiben, die von der Thread-Priorität abhängt (man kann sie nicht vorhersagen).
- Sich auf die Reihenfolge der Thread-Ausführung zu verlassen – selbst wenn ein Thread zuerst gestartet wird, gibt es keine Garantie, dass er zuerst ausgeführt wird.
Wichtige Aspekte zu Java-Threads
- Die
start()
-Methode aufrufen, um einenThread
zu starten. - Es ist möglich, die
Thread
-Klasse direkt zu erweitern, um Threads zu verwenden. - Es ist möglich, eine Thread-Aktion innerhalb eines
Runnable
-Interfaces zu implementieren. - Die Thread-Priorität hängt von der JVM-Implementierung ab.
- Das Verhalten von Threads hängt immer von der JVM-Implementierung ab.
- Ein Daemon-Thread wird nicht abgeschlossen, wenn ein umschließender Non-Daemon-Thread zuerst endet.
Fazit
Das Arbeiten mit Threads in Java ist ein komplexes, aber wesentliches Thema, um die Leistungsfähigkeit und Effizienz von Anwendungen zu verbessern. Durch das Verständnis des Lebenszyklus eines Threads, die Unterschiede zwischen Daemon- und Non-Daemon-Threads sowie die Bedeutung der Thread-Priorität und deren Abhängigkeit von der JVM-Implementierung können Entwickler effektivere und robustere Anwendungen erstellen. Dennoch ist Vorsicht geboten, da viele häufige Fehler gemacht werden können, die zu unerwartetem Verhalten führen. Letztendlich ist die Vorhersage der Thread-Ausführungsreihenfolge in der JVM eine Herausforderung, die tiefes Wissen und sorgfältige Planung erfordert.