Java 101: Grundlegendes zu Java-Threads, Teil 3: Thread-Planung und Warten / Benachrichtigen

Diesen Monat setze ich meine vierteilige Einführung in Java-Threads fort, indem ich mich auf die Thread-Planung, den Warte- / Benachrichtigungsmechanismus und die Thread-Unterbrechung konzentriere. Sie werden untersuchen, wie entweder ein JVM- oder ein Betriebssystem-Thread-Scheduler den nächsten Thread zur Ausführung auswählt. Wie Sie feststellen werden, ist die Priorität für die Auswahl eines Thread-Schedulers wichtig. Sie werden untersuchen, wie ein Thread wartet, bis er eine Benachrichtigung von einem anderen Thread erhält, bevor er die Ausführung fortsetzt, und lernen, wie der Warte- / Benachrichtigungsmechanismus zum Koordinieren der Ausführung von zwei Threads in einer Producer-Consumer-Beziehung verwendet wird. Schließlich lernen Sie, wie Sie entweder einen schlafenden oder einen wartenden Thread für die Beendigung des Threads oder andere Aufgaben vorzeitig aktivieren. Ich werde Ihnen auch beibringen, wie ein Thread, der weder schläft noch wartet, eine Unterbrechungsanforderung von einem anderen Thread erkennt.

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, Warten / Benachrichtigen und Thread-Unterbrechung
  • Teil 4: Thread-Gruppen, Volatilität, Thread-lokale Variablen, Timer und Thread-Tod

Thread-Planung

In einer idealisierten Welt hätten alle Programmthreads ihre eigenen Prozessoren, auf denen sie ausgeführt werden könnten. Bis zu dem Zeitpunkt, an dem Computer Tausende oder Millionen von Prozessoren haben, müssen Threads häufig einen oder mehrere Prozessoren gemeinsam nutzen. Entweder die JVM oder das Betriebssystem der zugrunde liegenden Plattform entschlüsseln, wie die Prozessorressource unter Threads aufgeteilt wird - eine Aufgabe, die als Thread-Planung bezeichnet wird . Der Teil der JVM oder des Betriebssystems, der die Thread-Planung durchführt, ist ein Thread-Scheduler .

Hinweis: Um meine Diskussion über die Thread-Planung zu vereinfachen, konzentriere ich mich auf die Thread-Planung im Kontext eines einzelnen Prozessors. Sie können diese Diskussion auf mehrere Prozessoren übertragen. Ich überlasse diese Aufgabe Ihnen.

Beachten Sie zwei wichtige Punkte zur Thread-Planung:

  1. Java zwingt eine VM nicht dazu, Threads auf eine bestimmte Weise zu planen oder einen Thread-Scheduler zu enthalten. Dies impliziert eine plattformabhängige Thread-Planung. Daher müssen Sie beim Schreiben eines Java-Programms, dessen Verhalten davon abhängt, wie Threads geplant sind, vorsichtig sein und auf verschiedenen Plattformen konsistent arbeiten.
  2. Glücklicherweise müssen Sie beim Schreiben von Java-Programmen darüber nachdenken, wie Java Threads nur dann plant, wenn mindestens einer der Threads Ihres Programms den Prozessor für lange Zeiträume stark beansprucht und sich Zwischenergebnisse der Ausführung dieses Threads als wichtig erweisen. Ein Applet enthält beispielsweise einen Thread, der dynamisch ein Bild erstellt. In regelmäßigen Abständen soll der Malfaden den aktuellen Inhalt des Bildes zeichnen, damit der Benutzer sehen kann, wie das Bild fortschreitet. Berücksichtigen Sie die Thread-Planung, um sicherzustellen, dass der Berechnungsthread den Prozessor nicht monopolisiert.

Untersuchen Sie ein Programm, das zwei prozessorintensive Threads erstellt:

Listing 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; 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; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemoErstellt zwei Threads, die jeweils den Wert von pi berechnen (fünfmal) und jedes Ergebnis drucken. Abhängig davon, wie Ihre JVM-Implementierung Threads plant, sehen Sie möglicherweise eine Ausgabe, die der folgenden ähnelt:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Gemäß der obigen Ausgabe teilt der Thread-Scheduler den Prozessor zwischen beiden Threads. Sie könnten jedoch eine ähnliche Ausgabe sehen:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Die obige Ausgabe zeigt den Thread-Scheduler, der einen Thread einem anderen vorzieht. Die beiden obigen Ausgaben veranschaulichen zwei allgemeine Kategorien von Thread-Schedulern: grün und nativ. Ich werde ihre Verhaltensunterschiede in den kommenden Abschnitten untersuchen. Während ich jede Kategorie diskutiere, beziehe ich mich auf Thread-Zustände, von denen es vier gibt:

  1. Ausgangszustand: Ein Programm hat das Thread-Objekt eines Threads erstellt, der Thread ist jedoch noch nicht vorhanden, da die start()Methode des Thread-Objekts noch nicht aufgerufen wurde.
  2. Ausführbarer Status: Dies ist der Standardstatus eines Threads. Nach Abschluss des Aufrufs von start()wird ein Thread ausgeführt, unabhängig davon, ob dieser Thread ausgeführt wird oder nicht, dh unter Verwendung des Prozessors. Obwohl viele Threads möglicherweise ausgeführt werden können, wird derzeit nur einer ausgeführt. Thread-Scheduler bestimmen, welcher ausführbare Thread dem Prozessor zugewiesen werden soll.
  3. Blockierten Zustand: Wenn ein Thread die ausführt sleep(), wait()oder join()Verfahren, wenn ein Thread versucht , Daten zu lesen , noch nicht verfügbar von einem Netzwerk, und wenn ein Thread wartet , eine Sperre zu erwerben, das Gewinde ist in dem gesperrten Zustand: sie weder ausgeführt noch in der Lage zu rennen. (Sie können sich wahrscheinlich andere Zeiten vorstellen, in denen ein Thread auf etwas warten würde.) Wenn ein blockierter Thread entsperrt wird, wechselt dieser Thread in den ausführbaren Zustand.
  4. Beendigungsstatus: Sobald die Ausführung die run()Methode eines Threads verlässt , befindet sich dieser Thread im Beendigungsstatus. Mit anderen Worten, der Thread hört auf zu existieren.

Wie wählt der Thread-Scheduler den ausführbaren Thread aus, der ausgeführt werden soll? Ich beginne diese Frage zu beantworten, während ich über die Planung von grünen Threads spreche. Ich beende die Antwort, während ich die native Thread-Planung diskutiere.

Green thread scheduling

Not all operating systems, the ancient Microsoft Windows 3.1 perating system, for example, support threads. For such systems, Sun Microsystems can design a JVM that divides its sole thread of execution into multiple threads. The JVM (not the underlying platform's operating system) supplies the threading logic and contains the thread scheduler. JVM threads are green threads, or user threads.

A JVM's thread scheduler schedules green threads according to priority—a thread's relative importance, which you express as an integer from a well-defined range of values. Typically, a JVM's thread scheduler chooses the highest-priority thread and allows that thread to run until it either terminates or blocks. At that time, the thread scheduler chooses a thread of the next highest priority. That thread (usually) runs until it terminates or blocks. If, while a thread runs, a thread of higher priority unblocks (perhaps the higher-priority thread's sleep time expired), the thread scheduler preempts, or interrupts, the lower-priority thread and assigns the unblocked higher-priority thread to the processor.

Hinweis: Ein ausführbarer Thread mit der höchsten Priorität wird nicht immer ausgeführt. Hier ist die Priorität der Java-Sprachspezifikation :

Jeder Thread hat eine Priorität. Wenn Wettbewerb um die Verarbeitung von Ressourcen besteht, werden Threads mit höherer Priorität im Allgemeinen gegenüber Threads mit niedrigerer Priorität bevorzugt ausgeführt. Eine solche Präferenz ist jedoch keine Garantie dafür, dass der Thread mit der höchsten Priorität immer ausgeführt wird, und Thread-Prioritäten können nicht verwendet werden, um den gegenseitigen Ausschluss zuverlässig zu implementieren.

That admission says much about the implementation of green thread JVMs. Those JVMs cannot afford to let threads block because that would tie up the JVM's sole thread of execution. Therefore, when a thread must block, such as when that thread is reading data slow to arrive from a file, the JVM might stop the thread's execution and use a polling mechanism to determine when data arrives. While the thread remains stopped, the JVM's thread scheduler might schedule a lower-priority thread to run. Suppose data arrives while the lower-priority thread is running. Although the higher-priority thread should run as soon as data arrives, that doesn't happen until the JVM next polls the operating system and discovers the arrival. Hence, the lower-priority thread runs even though the higher-priority thread should run. ou need to worry about this situation only when you need real-time behavior from Java. But then Java is not a real-time operating system, so why worry?

To understand which runnable green thread becomes the currently running green thread, consider the following. Suppose your application consists of three threads: the main thread that runs the main() method, a calculation thread, and a thread that reads keyboard input. When there is no keyboard input, the reading thread blocks. Assume the reading thread has the highest priority and the calculation thread has the lowest priority. (For simplicity's sake, also assume that no other internal JVM threads are available.) Figure 1 illustrates the execution of these three threads.

At time T0, the main thread starts running. At time T1, the main thread starts the calculation thread. Because the calculation thread has a lower priority than the main thread, the calculation thread waits for the processor. At time T2, the main thread starts the reading thread. Because the reading thread has a higher priority than the main thread, the main thread waits for the processor while the reading thread runs. At time T3, the reading thread blocks and the main thread runs. At time T4, the reading thread unblocks and runs; the main thread waits. Finally, at time T5, the reading thread blocks and the main thread runs. This alternation in execution between the reading and main threads continues as long as the program runs. The calculation thread never runs because it has the lowest priority and thus starves for processor attention, a situation known as processor starvation.

We can alter this scenario by giving the calculation thread the same priority as the main thread. Figure 2 shows the result, beginning with time T2. (Prior to T2, Figure 2 is identical to Figure 1.)

At time T2, the reading thread runs while the main and calculation threads wait for the processor. At time T3, the reading thread blocks and the calculation thread runs, because the main thread ran just before the reading thread. At time T4, the reading thread unblocks and runs; the main and calculation threads wait. At time T5, the reading thread blocks and the main thread runs, because the calculation thread ran just before the reading thread. This alternation in execution between the main and calculation threads continues as long as the program runs and depends on the higher-priority thread running and blocking.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

Die Prioritätsinversion kann die Ausführung eines Threads mit höherer Priorität erheblich verzögern. Angenommen, Sie haben drei Threads mit den Prioritäten 3, 4 und 9. Der Thread der Priorität 3 wird ausgeführt und die anderen Threads werden blockiert. Angenommen, der Thread mit Priorität 3 greift nach einer Sperre und der Thread mit Priorität 4 wird entsperrt. Der Thread mit Priorität 4 wird zum aktuell ausgeführten Thread. Da der Thread mit Priorität 9 die Sperre erfordert, wartet er weiter, bis der Thread mit Priorität 3 die Sperre aufhebt. Der Thread mit Priorität 3 kann die Sperre jedoch erst aufheben, wenn der Thread mit Priorität 4 blockiert oder beendet wird. Infolgedessen verzögert der Thread mit Priorität 9 seine Ausführung.