Einführung
Multithreading ist seit seinen Anfängen in Java integriert. Werfen wir also einen kurzen Blick darauf, worum es beim Multithreading geht.
Nehmen wir als Ausgangspunkt die offizielle Lektion von Oracle: „
Lektion: Die „Hello World!“-Anwendung “. Ändern wir den Code unserer Hello World-Anwendung ein wenig wie folgt:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
ist ein Array von Eingabeparametern, die beim Programmstart übergeben werden. Speichern wir diesen Code in einer Datei mit einem Namen, der dem Namen der Klasse und der Erweiterung entspricht
.java
. Kompilieren wir mit dem Dienstprogramm
javac :
javac HelloWorldApp.java
Rufen Sie anschließend unseren Code mit einem Parameter auf, zum Beispiel Roger:
java HelloWorldApp Roger
Unser Code weist jetzt einen schwerwiegenden Fehler auf. Wenn wir kein Argument übergeben (d. h. einfach Java HelloWorldApp ausführen), erhalten wir eine Fehlermeldung:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
In einem Thread mit dem Namen ist eine Ausnahme (d. h. ein Fehler) aufgetreten
main
. Es stellt sich heraus, dass es in Java eine Art Thread gibt? Hier beginnt unsere Reise.
Java und Threads
Um zu verstehen, was ein Thread ist, müssen Sie verstehen, wie eine Java-Anwendung gestartet wird. Ändern wir unseren Code wie folgt:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Jetzt kompilieren wir es erneut mit Javac. Als nächstes führen wir der Einfachheit halber unseren Java-Code in einem separaten Fenster aus. Unter Windows können Sie dies folgendermaßen tun:
start java HelloWorldApp
. Sehen wir uns nun mithilfe des Dienstprogramms
jps an, welche Informationen uns Java mitteilen wird: Die erste Zahl ist die PID oder Prozess-ID, die Prozesskennung. Was ist ein Prozess?
Процесс — это совокупность Codeа и данных, разделяющих общее виртуальное Adresseное пространство.
Mit Hilfe von Prozessen wird die Ausführung verschiedener Programme voneinander isoliert: Jede Anwendung nutzt ihren eigenen Speicherbereich, ohne andere Programme zu beeinträchtigen. Ich rate Ihnen, den Artikel genauer zu lesen: „
https://habr.com/post/164487/ “. Ein Prozess kann nicht ohne Threads existieren. Wenn also ein Prozess existiert, existiert mindestens ein Thread darin. Wie passiert das in Java? Wenn wir ein Java-Programm ausführen, beginnt seine Ausführung mit der
main
. Wir betreten das Programm gewissermaßen, daher
main
wird diese spezielle Methode als Einstiegspunkt oder „Einstiegspunkt“ bezeichnet. Die Methode
main
muss immer
public static void
so sein, dass die Java Virtual Machine (JVM) mit der Ausführung unseres Programms beginnen kann. Weitere Einzelheiten finden Sie unter „
Warum ist die Java-Hauptmethode statisch? “. Es stellt sich heraus, dass der Java Launcher (java.exe oder javaw.exe) eine einfache Anwendung (einfache C-Anwendung) ist: Er lädt verschiedene DLLs, die eigentlich die JVM sind. Der Java-Launcher führt einen bestimmten Satz von JNI-Aufrufen (Java Native Interface) durch. JNI ist der Mechanismus, der die Welt der Java Virtual Machine und die Welt von C++ verbindet. Es stellt sich heraus, dass der Launcher nicht die JVM, sondern ihr Loader ist. Es kennt die richtigen Befehle, die zum Starten der JVM ausgeführt werden müssen. Kann die gesamte erforderliche Umgebung mithilfe von JNI-Aufrufen organisieren. Zu dieser Organisation der Umgebung gehört auch die Erstellung eines Hauptthreads, der üblicherweise als
main
. Um klarer zu sehen, welche Threads in einem Java-Prozess leben, verwenden wir das Programm
jvisualvm , das im JDK enthalten ist. Wenn wir die PID eines Prozesses kennen, können wir sofort Daten darüber öffnen:
jvisualvm --openpid айдипроцесса
Interessanterweise verfügt jeder Thread über einen eigenen, separaten Bereich im Speicher, der dem Prozess zugewiesen ist. Diese Speicherstruktur wird Stapel genannt. Ein Stapel besteht aus Frames. Ein Frame ist der Punkt, an dem eine Methode aufgerufen wird, ein Ausführungspunkt. Ein Frame kann auch als StackTraceElement dargestellt werden (siehe Java API für
StackTraceElement ). Weitere Informationen zum jedem Thread zugewiesenen Speicher finden Sie
hier .
Wenn wir uns die Java-API ansehen und nach dem Wort Thread suchen, werden wir sehen, dass es eine Klasse
java.lang.Thread gibt . Es ist diese Klasse, die einen Stream in Java darstellt, und mit ihr müssen wir arbeiten.
java.lang.Thread
Ein Thread in Java wird als Instanz der Klasse dargestellt
java.lang.Thread
. Es lohnt sich sofort zu verstehen, dass Instanzen der Thread-Klasse in Java selbst keine Threads sind. Dabei handelt es sich lediglich um eine Art API für Low-Level-Threads, die von der JVM und dem Betriebssystem verwaltet werden. Wenn wir die JVM mit dem Java-Launcher starten, erstellt sie einen Haupt-Thread mit einem Namen
main
und mehrere weitere Service-Threads. Wie im JavaDoc der Thread-Klasse angegeben:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
Es gibt zwei Arten von Threads: Daemons und Nicht-Daemons. Daemon-Threads sind Hintergrund-Threads (Dienst-Threads), die einige Arbeiten im Hintergrund ausführen. Dieser interessante Begriff bezieht sich auf „Maxwells Dämon“, über den Sie im Wikipedia-Artikel „
Dämonen “ mehr lesen können. Wie in der Dokumentation angegeben, führt die JVM das Programm (den Prozess) so lange aus, bis:
- Die Runtime.exit -Methode wird nicht aufgerufen
- Alle Nicht-Daemon-Threads haben ihre Arbeit abgeschlossen (sowohl ohne Fehler als auch mit ausgelösten Ausnahmen).
Daher das wichtige Detail: Daemon-Threads können bei jedem ausgeführten Befehl beendet werden. Daher kann die Integrität der darin enthaltenen Daten nicht gewährleistet werden. Daher eignen sich Daemon-Threads für einige Serviceaufgaben. In Java gibt es beispielsweise einen Thread, der für die Verarbeitung von Finalize-Methoden oder Threads im Zusammenhang mit dem Garbage Collector (GC) verantwortlich ist. Jeder Thread gehört zu einer Gruppe (
ThreadGroup ). Und Gruppen können ineinander übergehen und eine Hierarchie oder Struktur bilden.
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Mit Gruppen können Sie die Verwaltung von Abläufen optimieren und den Überblick behalten. Zusätzlich zu Gruppen verfügen Threads über einen eigenen Ausnahmehandler. Schauen wir uns ein Beispiel an:
public static void main(String []args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Ein Fehler ist aufgetreten: " + e.getMessage());
}
});
System.out.println(2/0);
}
Eine Division durch Null führt zu einem Fehler, der vom Handler abgefangen wird. Wenn Sie den Handler nicht selbst angeben, funktioniert die Standard-Handler-Implementierung, die den Fehlerstapel in StdError anzeigt. Weitere Informationen finden Sie in der Rezension
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ". Darüber hinaus hat der Thread eine Priorität. Weitere Informationen zu Prioritäten finden Sie in der Artikel „
Java-Thread-Priorität im Multithreading “.
Einen Thread erstellen
Wie in der Dokumentation angegeben, haben wir zwei Möglichkeiten, einen Thread zu erstellen. Die erste besteht darin, Ihren Erben zu schaffen. Zum Beispiel:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String []args){
Thread thread = new MyThread();
thread.start();
}
}
Wie Sie sehen können, wird die Aufgabe in der Methode
run
und der Thread in der Methode gestartet
start
. Sie sollten nicht verwechselt werden, denn... Wenn wir die Methode
run
direkt ausführen, wird kein neuer Thread gestartet. Es ist die Methode
start
, die die JVM auffordert, einen neuen Thread zu erstellen. Die Option mit einem Nachkommen von Thread ist schlecht, da wir Thread in die Klassenhierarchie einbeziehen. Der zweite Nachteil besteht darin, dass wir beginnen, gegen den Grundsatz der „alleinigen Verantwortung“ zu verstoßen. SOLID, weil Unsere Klasse ist gleichzeitig für die Verwaltung des Threads und für einige Aufgaben verantwortlich, die in diesem Thread ausgeführt werden müssen. Welches ist richtig? Die Antwort liegt in der Methode
run
, die wir überschreiben:
public void run() {
if (target != null) {
target.run();
}
}
Hier
target
sind einige
java.lang.Runnable
, die wir beim Erstellen einer Instanz der Klasse an Thread übergeben können. Deshalb können wir Folgendes tun:
public class HelloWorld{
public static void main(String []args){
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Runnable
Seit Java 1.8 ist es auch eine funktionale Schnittstelle. Dadurch können Sie Task-Code für Threads noch schöner schreiben:
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Gesamt
Ich hoffe also, dass aus dieser Geschichte klar wird, was ein Stream ist, wie er existiert und welche grundlegenden Operationen mit ihm ausgeführt werden können. Im
nächsten Teil lohnt es sich zu verstehen, wie Threads miteinander interagieren und wie ihr Lebenszyklus aussieht. #Wjatscheslaw
GO TO FULL VERSION