In der Schleife wird der Wert ohne Druckanweisung nicht von einem anderen Thread geändert

89

In meinem Code habe ich eine Schleife, die darauf wartet, dass ein Status von einem anderen Thread geändert wird. Der andere Thread funktioniert, aber meine Schleife sieht den geänderten Wert nie. Es wartet für immer. Wenn ich jedoch eine System.out.printlnAnweisung in die Schleife setze , funktioniert sie plötzlich! Warum?


Das Folgende ist ein Beispiel für meinen Code:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

Während die while-Schleife läuft, rufe ich deliverPizza()von einem anderen Thread auf, um die pizzaArrivedVariable festzulegen. Die Schleife funktioniert jedoch nur, wenn ich die System.out.println("waiting");Anweisung auskommentiere . Was ist los?

Boann
quelle

Antworten:

148

Die JVM darf davon ausgehen, dass andere Threads die pizzaArrivedVariable während der Schleife nicht ändern . Mit anderen Worten, es kann den pizzaArrived == falseTest 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 volatilegarantiert, dass verschiedene Threads die Auswirkungen der Änderungen des jeweils anderen Threads sehen. Dies verhindert, dass die JVM den Wert des pizzaArrivedTests zwischenspeichert oder außerhalb der Schleife hebt. Stattdessen muss jedes Mal der Wert der realen Variablen gelesen werden.

(Erstellt formeller volatileeine 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 volatileVariablen 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 volatilesie. 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.outist ein PrintStreamObjekt. Die Methoden von PrintStreamwerden wie folgt synchronisiert:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Die Synchronisation verhindert, pizzaArriveddass 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 printlnnach dem Festlegen pizzaArrivedund ein erneuter Aufruf vor dem Lesen pizzaArrivedwä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 printlnnach dem Festlegen nicht aufgerufen werden. Daher wird pizzaArriveddavon 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.outist 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 BlockingQueuewird 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 putund takeMethoden von BlockingQueuecan werfen InterruptedExceptions, 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 BlockingQueuesehr einfach zu bedienen.

Hier ist keine weitere Synchronisierung erforderlich, da a BlockingQueuesicherstellt, 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

Executors sind wie fertige BlockingQueues, 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, ExecutorServiceund 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 BlockingQueueoder 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.Timerhat seinen eigenen Hintergrund-Thread, der zum Ausführen seiner geplanten TimerTasks 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!

Boann
quelle
Sehr professionelle Antwort, nachdem ich dies gelesen habe, ist mir kein Missverständnis mehr im Gedächtnis geblieben, danke
Humoyun Ahmad
1
Tolle Antwort. Ich arbeite schon eine ganze Weile mit Java-Threads und habe hier noch etwas gelernt ( wait()gibt die Synchronisationssperre frei!).
Brimborium
Danke, Boann! Tolle Antwort, es ist wie ein vollständiger Artikel mit Beispielen! Ja, mochte auch "wait () gibt die Synchronisationssperre frei"
Kiryl Ivanou
java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } } @Boann, dieser Code hebt den pizzaArrived == falseTest nicht außerhalb der Schleife und die Schleife kann sehen, dass das Flag vom Hauptthread geändert wurde. Warum?
Gaußclb
1
@gaussclb Wenn Sie meinen, dass Sie eine Klassendatei dekompiliert haben, korrigieren Sie. Der Java-Compiler führt fast keine Optimierung durch. Das Heben erfolgt durch die JVM. Sie müssen den nativen Maschinencode zerlegen. Versuchen Sie: wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann