Warum sollte wait () immer innerhalb einer Schleife aufgerufen werden?

Ich habe gelesen, dass wir wait() innerhalb einer Schleife aufrufen sollten:

 while (!condition) { obj.wait(); } 

Es funktioniert gut ohne eine Schleife, also warum ist das?

   

Sie müssen es nicht nur wiederholen, sondern auch Ihren Zustand in der Schleife überprüfen. Java garantiert nicht, dass Ihr Thread nur durch einen notify () / notifyAll () – Aufruf oder den richtigen notify () / notifyAll () -Aufruf geweckt wird. Aufgrund dieser Eigenschaft funktioniert die loop-lose Version möglicherweise in Ihrer Entwicklungsumgebung und schlägt in der Produktionsumgebung unerwartet fehl.

Zum Beispiel warten Sie auf etwas:

 synchronized (theObjectYouAreWaitingOn) { while (!carryOn) { theObjectYouAreWaitingOn.wait(); } } 

Ein böser Faden kommt und:

 theObjectYouAreWaitingOn.notifyAll(); 

Wenn der böse Thread nicht mit dem carryOn kann, dann carryOn du einfach weiter auf den richtigen Client.

Bearbeiten: Einige weitere Beispiele hinzugefügt. Die Wartezeit kann unterbrochen werden. Es triggers InterruptedException aus, und Sie müssen die Wartezeit möglicherweise in einen try-catch umbrechen. Abhängig von Ihren geschäftlichen Anforderungen können Sie die Ausnahme beenden oder unterdrücken und weiter warten.

Es ist in der Dokumentation für Object.wait (long milis) beantwortet

Ein Thread kann auch aufwachen, ohne benachrichtigt, unterbrochen oder ausgelöscht zu werden, ein sogenanntes spurious wakeup. Während dies in der Praxis selten vorkommt, müssen Anwendungen dagegen vorgehen, indem sie nach der Bedingung suchen, die den Thread geweckt haben sollte, und weiter warten, wenn die Bedingung nicht erfüllt ist. Mit anderen Worten, Wartezeiten sollten immer in Schleifen auftreten, wie diese:

  synchronized (obj) { while () obj.wait(timeout); ... // Perform action appropriate to condition } 

(Weitere Informationen zu diesem Thema finden Sie in Abschnitt 3.2.3 in Doug Leas “Concurrent Programming in Java (Zweite Ausgabe)” (Addison-Wesley, 2000), oder Artikel 50 in Joshua Blochs “Effective Java Programming Language Guide” (Addison- Wesley, 2001).

Es könnte mehr geben als nur ein Arbeiter, der auf eine Bedingung wartet, um wahr zu werden.

Wenn zwei oder mehr Arbeiter wach werden (notifyAll), müssen sie den Zustand erneut überprüfen. andernfalls würden alle Arbeiter weiterarbeiten, obwohl möglicherweise nur Daten für einen von ihnen vorhanden sind.

Warum sollte wait () immer innerhalb einer Schleife aufgerufen werden?

Der Hauptgrund, warum while Schleifen so wichtig sind, sind Race-Bedingungen zwischen Threads. Sicherlich sind falsche Wakeups real und für bestimmte Architekturen sind sie üblich, aber Race Conditions sind ein viel wahrscheinlicherer Grund für die while Schleife.

Beispielsweise:

 synchronized (queue) { // this needs to be while while (queue.isEmpty()) { queue.wait(); } queue.remove(); } 

Mit dem obigen Code können zwei Consumer-Threads vorhanden sein. Wenn der Producer die hinzuzufügende queue sperrt, wird möglicherweise der Kunde Nr. 1 bei der synchronized Sperre blockiert, während der Kunde Nr. 2 in der queue wartet. Wenn das Element der Warteschlange hinzugefügt und angerufen wird, wird # 2 aus der Warteschlange in die queue verschoben, aber es befindet sich hinter dem # 1-Benutzer, der bereits für die Sperre gesperrt wurde. Dies bedeutet, dass der # 1-Consumer nach vorne gehen muss, um zuerst aus der queue zu rufen. Wenn die while Schleife nur ein if , dann würde eine Ausnahme auftreten, wenn Verbraucher # 2 die Sperre erhält und Aufrufe remove , weil die queue jetzt leer ist – der andere Verbraucher-Thread hat das Element bereits aus der Warteschlange entfernt. Nur weil es gemeldet wurde, muss sichergestellt werden, dass die queue aufgrund dieser Wettlaufsituation immer noch leer ist.

Das ist gut dokumentiert. Hier ist eine Webseite, die ich vor einiger Zeit erstellt habe und die den Rennzustand im Detail erklärt und einen Beispielcode enthält.

Ich glaube, ich habe @Grays Antwort bekommen.

Lassen Sie mich versuchen, das für Neulinge wie mich umzuformulieren und die Experten zu bitten, mich zu korrigieren, wenn ich falsch liege.

Verbraucher synchronisierter Block ::

 synchronized (queue) { // this needs to be while while (queue.isEmpty()) { queue.wait(); } queue.remove(); } 

Produzent synchronisiert Block ::

 synchronized(queue) { // producer produces inside the queue queue.notify(); } 

Angenommen, in der angegebenen Reihenfolge geschieht Folgendes:

1) Verbraucher # 2 gelangt in den verbraucher- synchronized Block und wartet, da die Warteschlange leer ist.

2) Jetzt erhält der Erzeuger die Sperre für die queue und fügt sie in die Warteschlange ein und ruft notify () auf.

Nun kann entweder der Verbraucher Nr. 1 gewählt werden, der darauf wartet, dass die queue zum ersten Mal in den synchronized Block eintritt

oder

Verbraucher # 2 kann gewählt werden, um zu laufen.

3) sagen wir, Verbraucher # 1 wird gewählt, um mit der Ausführung fortzufahren. Wenn es die Bedingung überprüft, wird es wahr sein und es wird remove() aus der Warteschlange remove() .

4) sagen, Verbraucher # 2 geht von dort aus, wo es seine Ausführung angehalten hat (die Zeile nach der wait() -Methode). Wenn die ‘while’-Bedingung nicht vorhanden ist (stattdessen eine if Bedingung), fährt sie einfach mit dem Aufruf von remove() was zu einem Ausnahme- / unerwarteten Verhalten führen kann.

Da wait und notify verwendet werden, um [Bedingungsvariablen] ( http://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables) zu implementieren, müssen Sie überprüfen, ob das bestimmte Prädikat, auf das Sie warten, vorher wahr ist auch weiterhin.

Sowohl Sicherheit als auch Lebendigkeit sind Bedenken, wenn der Wartemeldungs- / Benachrichtigungsmechanismus verwendet wird. Die Sicherheitseigenschaft erfordert, dass alle Objekte in einer Multithread-Umgebung konsistente Zustände beibehalten. Die Liveness-Eigenschaft erfordert, dass jeder Vorgang oder Methodenaufruf ohne Unterbrechung ausgeführt wird.

Um die Lebendigkeit zu garantieren, müssen Programme die while-Schleife testen, bevor sie die wait () -Methode aufrufen. Dieser frühe Test überprüft, ob ein anderer Thread das Bedingungsprädikat bereits erfüllt und eine Benachrichtigung gesendet hat. Das Aufrufen der Methode wait () nach dem Senden der Benachrichtigung führt zu einer unbestimmten Blockierung.

Um die Sicherheit zu gewährleisten, müssen Programme die while-Schleife testen, nachdem sie von der Methode wait () zurückgekehrt sind. Obwohl wait () so lange blockiert werden soll, bis eine Benachrichtigung empfangen wird, muss sie dennoch in eine Schleife eingeschlossen werden, um folgende Sicherheitslücken zu vermeiden:

Thread in der Mitte: Ein dritter Thread kann die Sperre für das gemeinsam genutzte Objekt während des Intervalls zwischen einer gesendeten und der fortlaufenden Ausführung des empfangenden Threads übernehmen. Dieser dritte Thread kann den Status des Objekts ändern und es inkonsistent lassen. Dies ist eine Race-Bedingung für den Zeitpunkt der Überprüfung, Nutzungszeit (TOCTOU).

Böswillige Benachrichtigung: Eine zufällige oder bösartige Benachrichtigung kann empfangen werden, wenn das Bedingungsprädikat falsch ist. Eine solche Benachrichtigung würde die Methode wait () abbrechen.

Fehlgeschlagene Benachrichtigung: Die Reihenfolge, in der Threads nach Empfang eines notifyAll () – Signals ausgeführt werden, ist nicht angegeben. Folglich kann ein nicht verwandter Thread mit der Ausführung beginnen und feststellen, dass sein Bedingungsprädikat erfüllt ist. Folglich könnte es die Ausführung fortsetzen, obwohl es erforderlich ist, ruhend zu bleiben.

Falsche Wakeups: Bestimmte Java Virtual Machine (JVM) -Implementierungen sind anfällig für unechte Wakeups, die dazu führen, dass wartende Threads auch ohne Benachrichtigung aufwachen.

Aus diesen Gründen müssen Programme das Bedingungsprädikat überprüfen, nachdem die Methode wait () zurückgegeben wurde. Eine while-Schleife ist die beste Wahl, um das Bedingungsprädikat vor und nach dem Aufruf von wait () zu überprüfen.

In ähnlicher Weise muss die await () – Methode der Condition-Schnittstelle auch innerhalb einer Schleife aufgerufen werden. Gemäß der Java-API, Schnittstellenbedingung

Wenn auf eine Bedingung gewartet wird, kann im Allgemeinen als eine Zugeständnisse an die zugrundeliegende Plattform-Semantik ein “spurious wakeup” auftreten. Dies hat wenig praktische Auswirkungen auf die meisten Anwendungsprogramme, da eine Bedingung immer in einer Schleife abgewartet werden sollte, um das Zustandsprädikat zu testen, auf das gewartet wird. Eine Implementierung ist frei, die Möglichkeit von unechten Wakeups zu entfernen, aber es wird empfohlen, dass Anwendungsprogrammierer immer annehmen, dass sie auftreten können, und so immer in einer Schleife warten.

Neuer Code sollte die java.util.concurrent.locks-Nebenläufigkeitsdienstprogramme anstelle des Wartungs- / Benachrichtigungsmechanismus verwenden. Legacycode, der den anderen Anforderungen dieser Regel entspricht, darf jedoch vom Wartungs- / Benachrichtigungsmechanismus abhängen.

Nicht kompatibles Codebeispiel Dieses nicht kompatible Codebeispiel ruft die wait () -Methode in einem herkömmlichen if-Block auf und kann die Postkondition nicht überprüfen, nachdem die Benachrichtigung empfangen wurde. Wenn die Benachrichtigung zufällig oder bösartig war, konnte der Thread vorzeitig aufwachen.

 synchronized (object) { if () { object.wait(); } // Proceed when condition holds } 

Kompatible Lösung Diese kompatible Lösung ruft die wait () – Methode innerhalb einer while-Schleife auf, um die Bedingung vor und nach dem Aufruf von wait () zu prüfen:

 synchronized (object) { while () { object.wait(); } // Proceed when condition holds } 

Aufrufe der Methode java.util.concurrent.locks.Condition.await () müssen ebenfalls in eine ähnliche Schleife eingeschlossen sein.

Von deiner Frage:

Ich habe gelesen, dass wir wait () immer innerhalb einer Schleife aufrufen sollten:

Obwohl wait () normalerweise so lange wartet, bis notify () oder notifyAll () aufgerufen wird, besteht die Möglichkeit, dass in sehr seltenen Fällen der wartende Thread aufgrund eines unechten Wakeup geweckt werden kann. In diesem Fall wird ein wartender Thread fortgesetzt, ohne dass notify () oder notifyAll () aufgerufen wurde.

Im Wesentlichen wird der Thread ohne ersichtlichen Grund fortgesetzt.

Aufgrund dieser Remote-Möglichkeit empfiehlt Oracle, dass Aufrufe von wait () innerhalb einer Schleife stattfinden sollen, die die Bedingung überprüft, auf die der Thread wartet.