Die JVM darf davon ausgehen, dass andere Threads die pizzaArrived
Variable während der Schleife nicht ändern . Mit anderen Worten, es kann den pizzaArrived == false
Test außerhalb der Schleife heben und dies optimieren:
while (pizzaArrived == false) {}
das mögen:
if (pizzaArrived == false) while (true) {}
Das ist eine Endlosschleife.
Um sicherzustellen, dass von einem Thread vorgenommene Änderungen für andere Threads sichtbar sind, müssen Sie immer eine gewisse Synchronisation zwischen den Threads hinzufügen . Der einfachste Weg, dies zu tun, besteht darin, die gemeinsam genutzte Variable zu erstellen volatile
:
volatile boolean pizzaArrived = false;
Durch das Erstellen einer Variablen wird volatile
garantiert, dass verschiedene Threads die Auswirkungen der Änderungen des jeweils anderen Threads sehen. Dies verhindert, dass die JVM den Wert des pizzaArrived
Tests zwischenspeichert oder außerhalb der Schleife hebt. Stattdessen muss jedes Mal der Wert der realen Variablen gelesen werden.
(Erstellt formeller volatile
eine Beziehung zwischen den Zugriffen auf die Variable, bevor dies geschieht . Dies bedeutet, dass alle anderen Arbeiten, die ein Thread vor dem Ausliefern der Pizza ausgeführt hat, auch für den Thread sichtbar sind, der die Pizza empfängt, selbst wenn diese anderen Änderungen keine volatile
Variablen betreffen.)
Synchronisierte Methoden werden hauptsächlich verwendet, um den gegenseitigen Ausschluss zu implementieren (um zu verhindern, dass zwei Dinge gleichzeitig passieren), aber sie haben auch dieselben Nebenwirkungen wie volatile
sie. Die Verwendung beim Lesen und Schreiben einer Variablen ist eine weitere Möglichkeit, die Änderungen für andere Threads sichtbar zu machen:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
Die Wirkung einer Druckanweisung
System.out
ist ein PrintStream
Objekt. Die Methoden von PrintStream
werden wie folgt synchronisiert:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
Die Synchronisation verhindert, pizzaArrived
dass während der Schleife zwischengespeichert wird. Genau genommen müssen beide Threads auf demselben Objekt synchronisiert werden, um sicherzustellen, dass Änderungen an der Variablen sichtbar sind. (Ein Aufruf println
nach dem Festlegen pizzaArrived
und ein erneuter Aufruf vor dem Lesen pizzaArrived
wäre beispielsweise korrekt.) Wenn nur ein Thread für ein bestimmtes Objekt synchronisiert wird, kann die JVM es ignorieren. In der Praxis ist die JVM nicht intelligent genug, um zu beweisen, dass andere Threads println
nach dem Festlegen nicht aufgerufen werden. Daher wird pizzaArrived
davon ausgegangen, dass dies der Fall ist. Daher kann die Variable während der Schleife nicht zwischengespeichert werden, wenn Sie sie aufrufen System.out.println
. Aus diesem Grund funktionieren Schleifen wie diese, wenn sie eine print-Anweisung haben, obwohl dies keine korrekte Lösung ist.
Die Verwendung System.out
ist nicht der einzige Weg, um diesen Effekt zu verursachen, aber es ist derjenige, den die Leute am häufigsten entdecken, wenn sie versuchen zu debuggen, warum ihre Schleife nicht funktioniert!
Das größere Problem
while (pizzaArrived == false) {}
ist eine Busy-Wait-Schleife. Das ist schlecht! Während es wartet, belastet es die CPU, was andere Anwendungen verlangsamt und den Stromverbrauch, die Temperatur und die Lüftergeschwindigkeit des Systems erhöht. Im Idealfall möchten wir, dass der Loop-Thread während des Wartens in den Ruhezustand versetzt wird, damit die CPU nicht belastet wird.
Hier sind einige Möglichkeiten, dies zu tun:
Mit wait / notify
Eine einfache Lösung besteht darin , die folgenden Warte- / Benachrichtigungsmethoden zu verwendenObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
In dieser Version des Codes ruft der Schleifenthread auf wait()
, wodurch der Thread in den Ruhezustand versetzt wird. Im Ruhezustand werden keine CPU-Zyklen verwendet. Nachdem der zweite Thread die Variable gesetzt hat, ruft er notifyAll()
auf, um alle Threads zu aktivieren, die auf dieses Objekt gewartet haben. Dies ist so, als würde der Pizzabote an der Tür klingeln, sodass Sie sich hinsetzen und ausruhen können, während Sie warten, anstatt unbeholfen an der Tür zu stehen.
Wenn Sie wait / notify für ein Objekt aufrufen, müssen Sie die Synchronisationssperre dieses Objekts gedrückt halten, wie im obigen Code beschrieben. Sie können jedes beliebige Objekt verwenden, solange beide Threads dasselbe Objekt verwenden: hier habe ich this
(die Instanz von MyHouse
) verwendet. Normalerweise können zwei Threads nicht gleichzeitig synchronisierte Blöcke desselben Objekts eingeben (was Teil des Synchronisationszwecks ist), aber dies funktioniert hier, da ein Thread die Synchronisationssperre vorübergehend aufhebt, wenn er sich innerhalb der wait()
Methode befindet.
BlockingQueue
A BlockingQueue
wird verwendet, um Produzenten-Konsumenten-Warteschlangen zu implementieren. "Verbraucher" nehmen Artikel von der Vorderseite der Warteschlange und "Produzenten" schieben Artikel von hinten auf. Ein Beispiel:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
// take next item from the queue (sleeps while waiting)
Object food = queue.take();
// and do something with it
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
// in producer threads, we push items on to the queue.
// if there is space in the queue we can return immediately;
// the consumer thread(s) will get to it later
queue.put("A delicious pizza");
}
}
Hinweis: Die put
und take
Methoden von BlockingQueue
can werfen InterruptedException
s, dies sind geprüfte Ausnahmen, die behandelt werden müssen. Im obigen Code werden der Einfachheit halber die Ausnahmen erneut ausgelöst. Möglicherweise möchten Sie die Ausnahmen in den Methoden abfangen und den Put- oder Take-Aufruf erneut versuchen, um sicherzustellen, dass er erfolgreich ist. Abgesehen davon ist ein Punkt der Hässlichkeit BlockingQueue
sehr einfach zu bedienen.
Hier ist keine weitere Synchronisierung erforderlich, da a BlockingQueue
sicherstellt, dass alle Threads, die vor dem Einfügen von Elementen in die Warteschlange ausgeführt wurden, für die Threads sichtbar sind, die diese Elemente entfernen.
Ausführende
Executor
s sind wie fertige BlockingQueue
s, die Aufgaben ausführen. Beispiel:
// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish
Weitere Einzelheiten finden Sie in der doc für Executor
, ExecutorService
und Executors
.
Handhabung des Events
Das Schleifen, während darauf gewartet wird, dass der Benutzer auf etwas in einer Benutzeroberfläche klickt, ist falsch. Verwenden Sie stattdessen die Ereignisbehandlungsfunktionen des UI-Toolkits. In Swing zum Beispiel:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
// This event listener is run when the button is clicked.
// We don't need to loop while waiting.
label.setText("Button was clicked");
});
Da der Ereignishandler im Ereignisversand-Thread ausgeführt wird, blockiert die lange Arbeit im Ereignishandler die andere Interaktion mit der Benutzeroberfläche, bis die Arbeit abgeschlossen ist. Langsame Vorgänge können für einen neuen Thread gestartet oder mit einer der oben genannten Techniken (wait / notify, a BlockingQueue
oder Executor
) an einen wartenden Thread gesendet werden . Sie können auch ein verwenden SwingWorker
, das genau dafür ausgelegt ist und automatisch einen Hintergrund-Worker-Thread bereitstellt:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {
// Defines MyWorker as a SwingWorker whose result type is String:
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
// This method is called on a background thread.
// You can do long work here without blocking the UI.
// This is just an example:
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
// This method is called on the Swing thread once the work is done
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result); // will display "Answer is 42"
}
}
// Start the worker
new MyWorker().execute();
});
Timer
Um regelmäßige Aktionen auszuführen, können Sie a verwenden java.util.Timer
. Es ist einfacher zu verwenden als eine eigene Zeitschleife zu schreiben und einfacher zu starten und zu stoppen. Diese Demo druckt die aktuelle Zeit einmal pro Sekunde:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Jeder java.util.Timer
hat seinen eigenen Hintergrund-Thread, der zum Ausführen seiner geplanten TimerTask
s verwendet wird. Natürlich schläft der Thread zwischen den Aufgaben, sodass die CPU nicht belastet wird.
Im Swing-Code gibt es auch einen javax.swing.Timer
, der ähnlich ist, aber den Listener im Swing-Thread ausführt, sodass Sie sicher mit Swing-Komponenten interagieren können, ohne die Threads manuell wechseln zu müssen:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
Andere Möglichkeiten
Wenn Sie Multithread-Code schreiben, sollten Sie die Klassen in diesen Paketen untersuchen, um festzustellen, was verfügbar ist:
Weitere Informationen finden Sie im Abschnitt Parallelität der Java-Tutorials. Multithreading ist kompliziert, aber es gibt viel Hilfe!