Java 101: Grundlegendes zu Java-Threads, Teil 1: Einführung in Threads und ausführbare Dateien

Dieser Artikel ist der erste in einer vierteiligen Java 101- Reihe, in der Java-Threads untersucht werden. Obwohl Sie vielleicht denken, dass das Threading in Java schwierig zu verstehen ist, möchte ich Ihnen zeigen, dass Threads leicht zu verstehen sind. In diesem Artikel stelle ich Ihnen Java-Threads und Runnables vor. In den folgenden Artikeln werden Synchronisation (über Sperren), Synchronisationsprobleme (wie Deadlock), Warte- / Benachrichtigungsmechanismus, Zeitplanung (mit und ohne Priorität), Thread-Unterbrechung, Timer, Volatilität, Thread-Gruppen und lokale Thread-Variablen untersucht .

Beachten Sie, dass dieser Artikel (Teil des JavaWorld-Archivs) im Mai 2013 mit neuen Codelisten und herunterladbarem Quellcode aktualisiert wurde.

Grundlegendes zu Java-Threads - Lesen Sie die gesamte Serie

  • Teil 1: Einführung in Threads und Runnables
  • Teil 2: Synchronisation
  • Teil 3: Thread-Planung und Warten / Benachrichtigen
  • Teil 4: Fadengruppen und Volatilität

Was ist ein Thread?

Konzeptionell ist der Begriff eines Threads nicht schwer zu verstehen: Es handelt sich um einen unabhängigen Ausführungspfad durch den Programmcode. Wenn mehrere Threads ausgeführt werden, unterscheidet sich der Pfad eines Threads durch denselben Code normalerweise von den anderen. Angenommen, ein Thread führt das Bytecode-Äquivalent des ifTeils einer if-else-Anweisung aus , während ein anderer Thread das Bytecode-Äquivalent des elseTeils ausführt . Wie verfolgt die JVM die Ausführung jedes Threads? Die JVM gibt jedem Thread einen eigenen Methodenaufrufstapel. Zusätzlich zum Verfolgen des aktuellen Bytecode-Befehls verfolgt der Methodenaufrufstapel lokale Variablen, Parameter, die die JVM an eine Methode übergibt, und den Rückgabewert der Methode.

Wenn mehrere Threads Bytecode-Befehlssequenzen in demselben Programm ausführen, wird diese Aktion als Multithreading bezeichnet . Multithreading kommt einem Programm auf verschiedene Weise zugute:

  • Programme mit Multithread-GUI (grafische Benutzeroberfläche) reagieren weiterhin auf Benutzer, während sie andere Aufgaben ausführen, z. B. das Repaginieren oder Drucken eines Dokuments.
  • Threaded-Programme werden normalerweise schneller beendet als ihre Gegenstücke ohne Thread. Dies gilt insbesondere für Threads, die auf einem Multiprozessor-Computer ausgeführt werden, auf dem jeder Thread über einen eigenen Prozessor verfügt.

Java führt Multithreading durch seine java.lang.ThreadKlasse durch. Jedes ThreadObjekt beschreibt einen einzelnen Ausführungsthread. Diese Ausführung erfolgt in Threadder run()Methode von. Da die Standardmethode run()nichts bewirkt, müssen Sie eine Unterklasse erstellen Threadund überschreiben run(), um nützliche Arbeit zu leisten. In ThreadListing 1 finden Sie Informationen zu Threads und Multithreading im Kontext von :

Listing 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

Listing 1 zeigt den Quellcode einer Anwendung, die aus Klassen ThreadDemound besteht MyThread. Die Klasse ThreadDemosteuert die Anwendung, indem sie ein MyThreadObjekt erstellt, einen Thread startet, der diesem Objekt zugeordnet ist, und Code zum Drucken einer Quadrattabelle ausführt. Im Gegensatz dazu MyThreadüberschreibt Thread‚s - run()Verfahren (auf dem Standardausgabestrom) zu drucken , ein rechtwinkliges Dreieck , bestehend aus Stern - Zeichen.

Thread-Planung und die JVM

Die meisten (wenn nicht alle) JVM-Implementierungen verwenden die Threading-Funktionen der zugrunde liegenden Plattform. Da diese Funktionen plattformspezifisch sind, kann die Reihenfolge der Ausgabe Ihrer Multithread-Programme von der Reihenfolge der Ausgabe anderer Personen abweichen. Dieser Unterschied ergibt sich aus der Planung, ein Thema, das ich später in dieser Reihe untersuche.

Wenn Sie eingeben java ThreadDemo, um die Anwendung auszuführen, erstellt die JVM einen Startthread für die Ausführung, der die main()Methode ausführt . Durch Ausführen mt.start ();weist der Startthread die JVM an, einen zweiten Ausführungsthread zu erstellen, der die Bytecode-Anweisungen ausführt, die MyThreaddie run()Methode des Objekts umfassen . Wenn die start()Methode zurückkehrt, führt der Start-Thread seine forSchleife aus, um eine Tabelle mit Quadraten zu drucken, während der neue Thread die run()Methode zum Drucken des rechtwinkligen Dreiecks ausführt .

Wie sieht die Ausgabe aus? Lauf ThreadDemoum es herauszufinden. Sie werden feststellen, dass die Ausgabe jedes Threads dazu neigt, sich mit der Ausgabe des anderen zu vermischen. Dies liegt daran, dass beide Threads ihre Ausgabe an denselben Standardausgabestream senden.

Die Thread-Klasse

Um Multithread-Code besser schreiben zu können, müssen Sie zunächst die verschiedenen Methoden der ThreadKlasse verstehen . In diesem Abschnitt werden viele dieser Methoden erläutert. Insbesondere lernen Sie Methoden zum Starten von Threads, zum Benennen von Threads, zum Deaktivieren von Threads, zum Bestimmen, ob ein Thread aktiv ist, zum Verbinden eines Threads mit einem anderen Thread und zum Auflisten aller aktiven Threads in der Threadgruppe und den Untergruppen des aktuellen Threads. Ich diskutiere auch Threaddie Debugging-Hilfsmittel und Benutzer-Threads im Vergleich zu Daemon-Threads.

Ich werde den Rest der ThreadMethoden in den folgenden Artikeln vorstellen , mit Ausnahme der veralteten Methoden von Sun.

Veraltete Methoden

Sun hat eine Vielzahl von ThreadMethoden wie suspend()und abgelehnt resume(), da sie Ihre Programme blockieren oder Objekte beschädigen können. Daher sollten Sie sie in Ihrem Code nicht aufrufen. In der SDK-Dokumentation finden Sie Problemumgehungen für diese Methoden. Ich gehe nicht auf veraltete Methoden in dieser Serie ein.

Fäden konstruieren

Threadhat acht Konstruktoren. Die einfachsten sind:

  • Thread(), wodurch ein ThreadObjekt mit einem Standardnamen erstellt wird
  • Thread(String name), wodurch ein ThreadObjekt mit einem Namen erstellt wird, den das nameArgument angibt

Die nächst einfacheren Konstruktoren sind Thread(Runnable target)und Thread(Runnable target, String name). Abgesehen von den RunnableParametern sind diese Konstruktoren mit den oben genannten Konstruktoren identisch. Der Unterschied: Die RunnableParameter identifizieren Objekte außerhalb Thread, die die run()Methoden bereitstellen . (Sie lernen Runnablespäter in diesem Artikel.) Die letzten vier Konstrukteuren ähneln Thread(String name), Thread(Runnable target)und Thread(Runnable target, String name); Die endgültigen Konstruktoren enthalten jedoch auch ein ThreadGroupArgument für organisatorische Zwecke.

Einer der letzten vier Konstruktoren Thread(ThreadGroup group, Runnable target, String name, long stackSize)ist insofern interessant, als Sie die gewünschte Größe des Methodenaufrufstapels des Threads angeben können. Die Angabe dieser Größe erweist sich in Programmen mit Methoden, die Rekursion verwenden - eine Ausführungstechnik, bei der sich eine Methode wiederholt selbst aufruft - als hilfreich, um bestimmte Probleme elegant zu lösen. Durch explizites Festlegen der Stapelgröße können Sie manchmal StackOverflowErrors verhindern . Eine zu große Größe kann jedoch zu OutOfMemoryErrors führen. Außerdem betrachtet Sun die Größe des Methodenaufrufstapels als plattformabhängig. Je nach Plattform kann sich die Größe des Methodenaufrufstapels ändern. Denken Sie daher sorgfältig über die Auswirkungen auf Ihr Programm nach, bevor Sie den aufrufenden Code schreiben Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Starten Sie Ihre Fahrzeuge

Threads ähneln Fahrzeugen: Sie bewegen Programme von Anfang bis Ende. Threadund ThreadUnterklassenobjekte sind keine Threads. Stattdessen beschreiben sie die Attribute eines Threads, z. B. seinen Namen, und enthalten Code (über eine run()Methode), den der Thread ausführt. Wenn ein neuer Thread ausgeführt werden muss run(), ruft ein anderer Thread Threaddie start()Methode des Objekts oder seines Unterklassenobjekts auf . Um beispielsweise einen zweiten Thread zu starten, wird der main()Startthread der Anwendung, der ausgeführt wird, aufgerufen start(). Als Reaktion darauf arbeitet der Thread-Handling-Code der JVM mit der Plattform zusammen, um sicherzustellen, dass der Thread Threaddie run()Methode eines Objekts oder seines Unterklassenobjekts ordnungsgemäß initialisiert und aufruft .

Nach start()Abschluss werden mehrere Threads ausgeführt. Da wir dazu neigen, linear zu denken, fällt es uns oft schwer, die gleichzeitige (gleichzeitige) Aktivität zu verstehen, die auftritt, wenn zwei oder mehr Threads ausgeführt werden. Daher sollten Sie ein Diagramm untersuchen, das zeigt, wo ein Thread (seine Position) im Verhältnis zur Zeit ausgeführt wird. Die folgende Abbildung zeigt eine solche Tabelle.

Die Grafik zeigt mehrere wichtige Zeiträume:

  • Die Initialisierung des Startthreads
  • Der Moment, in dem der Thread ausgeführt wird main()
  • Der Moment, in dem der Thread ausgeführt wird start()
  • Der Moment start()erstellt einen neuen Thread und kehrt zu zurückmain()
  • Die Initialisierung des neuen Threads
  • In dem Moment, in dem der neue Thread ausgeführt wird run()
  • Die verschiedenen Momente, in denen jeder Thread endet

Note that the new thread's initialization, its execution of run(), and its termination happen simultaneously with the starting thread's execution. Also note that after a thread calls start(), subsequent calls to that method before the run() method exits cause start() to throw a java.lang.IllegalThreadStateException object.

What's in a name?

During a debugging session, distinguishing one thread from another in a user-friendly fashion proves helpful. To differentiate among threads, Java associates a name with a thread. That name defaults to Thread, a hyphen character, and a zero-based integer number. You can accept Java's default thread names or you can choose your own. To accommodate custom names, Thread provides constructors that take name arguments and a setName(String name) method. Thread also provides a getName() method that returns the current name. Listing 2 demonstrates how to establish a custom name via the Thread(String name) constructor and retrieve the current name in the run() method by calling getName():

Listing 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

You can pass an optional name argument to MyThread on the command line. For example, java NameThatThread X establishes X as the thread's name. If you fail to specify a name, you'll see the following output:

My name is: Thread-1

If you prefer, you can change the super (name); call in the MyThread (String name) constructor to a call to setName (String name)—as in setName (name);. That latter method call achieves the same objective—establishing the thread's name—as super (name);. I leave that as an exercise for you.

Naming main

Java assigns the name main to the thread that runs the main() method, the starting thread. You typically see that name in the Exception in thread "main" message that the JVM's default exception handler prints when the starting thread throws an exception object.

To sleep or not to sleep

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Zur Demonstration sleep(long millis)habe ich eine CalcPI1Bewerbung geschrieben. Diese Anwendung startet einen neuen Thread, der einen mathematischen Algorithmus verwendet, um den Wert der mathematischen Konstante pi zu berechnen. Während der Berechnung des neuen Threads wird der Start-Thread durch Aufrufen für 10 Millisekunden angehalten sleep(long millis). Nachdem der Startthread aktiviert wurde, wird der pi-Wert gedruckt, den der neue Thread in einer Variablen speichert pi. Listing 3 präsentiert CalcPI1den Quellcode:

Listing 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Wenn Sie dieses Programm ausführen, wird eine ähnliche (aber wahrscheinlich nicht identische) Ausgabe wie folgt angezeigt:

pi = -0.2146197014017295 Finished calculating PI