Warum muss wait () immer im synchronisierten Block sein


257

Wir alle wissen, dass Object.wait()dieser Aufruf zum Aufrufen in einen synchronisierten Block gestellt werden muss, andernfalls wird ein IllegalMonitorStateExceptionausgelöst. Aber was ist der Grund für diese Einschränkung? Ich weiß, dass wait()der Monitor freigegeben wird, aber warum müssen wir den Monitor explizit erfassen, indem wir einen bestimmten Block synchronisieren und dann den Monitor durch Aufrufen freigeben wait()?

Was ist der potenzielle Schaden, wenn es möglich war, wait()außerhalb eines synchronisierten Blocks aufzurufen und dessen Semantik beizubehalten - den Aufrufer-Thread anzuhalten?

Antworten:


232

A ist wait()nur dann sinnvoll, wenn es auch ein notify()gibt. Es geht also immer um die Kommunikation zwischen Threads, und das muss synchronisiert werden, damit es richtig funktioniert. Man könnte argumentieren, dass dies implizit sein sollte, aber das würde aus folgendem Grund nicht wirklich helfen:

Semantisch gesehen nie einfach wait(). Sie benötigen eine Bedingung, um zufrieden zu sein, und wenn dies nicht der Fall ist, warten Sie, bis dies der Fall ist. Was Sie also wirklich tun, ist

if(!condition){
    wait();
}

Die Bedingung wird jedoch von einem separaten Thread festgelegt. Damit dies ordnungsgemäß funktioniert, müssen Sie eine Synchronisierung durchführen.

Noch ein paar Dinge, die daran falsch sind. Nur weil Ihr Thread aufhört zu warten, bedeutet dies nicht, dass die gesuchte Bedingung wahr ist:

  • Sie können falsche Weckrufe erhalten (was bedeutet, dass ein Thread vom Warten aufwachen kann, ohne jemals eine Benachrichtigung erhalten zu haben) oder

  • Die Bedingung kann festgelegt werden, aber ein dritter Thread macht die Bedingung erneut falsch, wenn der wartende Thread aufwacht (und den Monitor erneut abruft).

Um mit diesen Fällen fertig zu werden, brauchen Sie immer eine Variation davon:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Besser noch, spielen Sie überhaupt nicht mit den Synchronisationsprimitiven und arbeiten Sie mit den in den java.util.concurrentPaketen angebotenen Abstraktionen .


3
Auch hier gibt es eine ausführliche Diskussion, die im Wesentlichen dasselbe sagt. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
Übrigens, wenn Sie das unterbrochene Flag nicht ignorieren sollen, wird die Schleife ebenfalls prüfen Thread.interrupted().
Bests

2
Ich kann immer noch so etwas tun wie: while (! Condition) {synchronized (this) {wait ();}}, was bedeutet, dass es immer noch einen Wettlauf zwischen dem Überprüfen der Bedingung und dem Warten gibt, selbst wenn wait () in einem synchronisierten Block korrekt aufgerufen wird. Gibt es also einen anderen Grund für diese Einschränkung, möglicherweise aufgrund der Art und Weise, wie sie in Java implementiert ist?
Shrini1000

9
Ein weiteres böses Szenario: Bedingung ist falsch, wir werden gleich warten () und dann ändert ein anderer Thread die Bedingung und ruft notify () auf. Da wir noch nicht warten (), werden wir diese Benachrichtigung () verpassen. Mit anderen Worten, Testen und Warten sowie Ändern und Benachrichtigen müssen atomar sein .

1
@Nullpointer: Wenn es sich um einen Typ handelt, der atomar geschrieben werden kann (z. B. der Boolesche Wert, der durch die direkte Verwendung in einer if-Klausel impliziert wird), und keine Interdependenz mit anderen gemeinsam genutzten Daten besteht, können Sie ihn als flüchtig deklarieren. Sie benötigen jedoch entweder diese oder eine Synchronisierung, um sicherzustellen, dass das Update für andere Threads sofort sichtbar ist.
Michael Borgwardt

283

Was ist der potenzielle Schaden, wenn es möglich war, wait()außerhalb eines synchronisierten Blocks aufzurufen und dessen Semantik beizubehalten - den Aufrufer-Thread anzuhalten?

Lassen Sie uns anhand wait()eines konkreten Beispiels veranschaulichen, auf welche Probleme wir stoßen würden, wenn sie außerhalb eines synchronisierten Blocks aufgerufen werden könnten .

Angenommen, wir würden eine Blockierungswarteschlange implementieren (ich weiß, es gibt bereits eine in der API :)

Ein erster Versuch (ohne Synchronisation) könnte etwas in der folgenden Richtung aussehen

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

Dies könnte möglicherweise passieren:

  1. Ein Consumer-Thread ruft auf take()und sieht, dass die buffer.isEmpty().

  2. Bevor der Consumer-Thread weiter aufgerufen wird wait(), kommt ein Producer-Thread und ruft einen vollständigen Thread auf give(), d. H.buffer.add(data); notify();

  3. Der Consumer-Thread ruft jetzt auf wait()(und übersieht den notify()gerade aufgerufenen).

  4. Wenn Sie Pech haben, wird der Producer-Thread nicht mehr produzieren give(), da der Consumer-Thread nie aufwacht und wir einen Deadlock haben.

Sobald Sie das Problem verstanden haben, liegt die Lösung auf der Hand: Verwenden Sie synchronizeddiese Option , um sicherzustellen, dass notifyniemals zwischen isEmptyund aufgerufen wird wait.

Ohne auf Details einzugehen: Dieses Synchronisationsproblem ist universell. Wie Michael Borgwardt betont, dreht sich beim Warten / Benachrichtigen alles um die Kommunikation zwischen Threads, sodass Sie immer eine ähnliche Rennbedingung haben wie oben beschrieben. Aus diesem Grund wird die Regel "Nur innerhalb synchronisiert warten" erzwungen.


Ein Absatz aus dem von @Willie geposteten Link fasst es recht gut zusammen:

Sie benötigen eine absolute Garantie dafür, dass sich der Kellner und der Notifizierende über den Status des Prädikats einig sind. Der Kellner überprüft den Status des Prädikats zu einem bestimmten Zeitpunkt geringfügig, bevor es in den Ruhezustand wechselt. Die Richtigkeit hängt jedoch davon ab, ob das Prädikat wahr ist, wenn es in den Ruhezustand wechselt. Zwischen diesen beiden Ereignissen besteht eine gewisse Sicherheitslücke, die das Programm beschädigen kann.

Das Prädikat, auf das sich Hersteller und Verbraucher einigen müssen, ist im obigen Beispiel dargestellt buffer.isEmpty(). Die Vereinbarung wird gelöst, indem sichergestellt wird, dass das Warten und Benachrichtigen in synchronizedBlöcken ausgeführt wird.


Dieser Beitrag wurde hier als Artikel umgeschrieben: Java: Warum warten muss in einem synchronisierten Block aufgerufen werden


Außerdem, um sicherzustellen, dass die an der Bedingung vorgenommenen Änderungen unmittelbar nach dem Ende von wait () angezeigt werden, denke ich. Ansonsten auch ein Deadlock seit dem Aufruf von notify ().
Surya Wijaya Madjid

Interessant, aber beachten Sie, dass nur ein synchronisierter Aufruf solche Probleme aufgrund der "unzuverlässigen" Natur von wait () und notify () nicht immer löst. Lesen Sie hier mehr: stackoverflow.com/questions/21439355/… . Der Grund, warum synchronisiert benötigt wird, liegt in der Hardwarearchitektur (siehe meine Antwort unten).
Marcus

aber wenn add return buffer.remove();in while block aber danach wait();funktioniert es?
BobJiang

@ BobJiang, nein, der Thread kann aus anderen Gründen als jemandem, der give anruft, geweckt werden. Mit anderen Worten, der Puffer kann auch nach der waitRückgabe leer sein .
Aioobe

Ich habe nur Thread.currentThread().wait();in der mainFunktion von Try-Catch für umgeben InterruptedException. Ohne synchronizedBlock gibt es mir die gleiche Ausnahme IllegalMonitorStateException. Was bringt es jetzt dazu, einen illegalen Staat zu erreichen? Es funktioniert jedoch innerhalb des synchronizedBlocks.
Shashwat

12

@ Rollerball ist richtig. Das wait()wird aufgerufen, damit der Thread warten kann, bis eine Bedingung auftritt, wenn dieser wait()Aufruf auftritt. Der Thread ist gezwungen, seine Sperre aufzugeben.
Um etwas aufzugeben, müssen Sie es zuerst besitzen. Der Thread muss zuerst das Schloss besitzen. Daher muss es innerhalb einer synchronizedMethode / eines Blocks aufgerufen werden .

Ja, ich stimme allen obigen Antworten in Bezug auf mögliche Schäden / Inkonsistenzen zu, wenn Sie den Zustand innerhalb der synchronizedMethode / des Blocks nicht überprüft haben . Wie jedoch @ shrini1000 hervorgehoben hat, verhindert das Aufrufen wait()innerhalb eines synchronisierten Blocks nicht, dass diese Inkonsistenz auftritt .

Hier ist eine schöne Lektüre ..


5
@Popeye Erklären Sie 'richtig' richtig. Ihr Kommentar nützt niemandem.
Marquis von Lorne

4

Das Problem, das auftreten kann, wenn Sie vorher nicht synchronisieren, wait()ist wie folgt:

  1. Wenn der erste Thread in makeChangeOnX()die while-Bedingung geht und diese überprüft und es ist true(gibt x.metCondition()zurück false, bedeutet x.conditionist false), wird es in ihn gelangen. Dann geht kurz vor der wait()Methode ein anderer Thread zu setConditionToTrue()und setzt das x.conditionzu trueund notifyAll().
  2. Erst danach wird der erste Thread in seine wait()Methode eingegeben (nicht betroffen von dem notifyAll(), was einige Momente zuvor passiert ist). In diesem Fall wartet der erste Thread auf die Ausführung eines anderen Threads, dies setConditionToTrue()kann jedoch möglicherweise nicht erneut auftreten.

Wenn Sie jedoch synchronizeddie Methoden vorlegen, die den Objektstatus ändern, wird dies nicht passieren.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Wir alle wissen, dass die Methoden wait (), notify () und notifyAll () für die Kommunikation zwischen Threads verwendet werden. Um verpasste Signale und falsche Weckprobleme zu beseitigen, wartet der wartende Thread unter bestimmten Bedingungen immer. z.B-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Das Benachrichtigen von Thread-Sets war die Variable NotNotified auf true und notify.

Jeder Thread hat seinen lokalen Cache, sodass alle Änderungen zuerst dort geschrieben und dann schrittweise in den Hauptspeicher befördert werden.

Wenn diese Methoden nicht innerhalb des synchronisierten Blocks aufgerufen würden, würde die Variable wasNotified nicht in den Hauptspeicher geleert und würde sich im lokalen Cache des Threads befinden, sodass der wartende Thread weiterhin auf das Signal wartet, obwohl es durch Benachrichtigen des Threads zurückgesetzt wurde.

Um diese Art von Problemen zu beheben, werden diese Methoden immer innerhalb des synchronisierten Blocks aufgerufen, wodurch sichergestellt wird, dass beim Start des synchronisierten Blocks alles aus dem Hauptspeicher gelesen und vor dem Verlassen des synchronisierten Blocks in den Hauptspeicher geleert wird.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Danke, hoffe es klärt.


1

Dies hat im Wesentlichen mit der Hardwarearchitektur (dh RAM und Caches ) zu tun .

Wenn Sie nicht verwenden , synchronizedzusammen mit wait()oder notify(), ein anderer Thread könnte den gleichen Block eingeben , anstatt für den Monitor des Wartens , es zu betreten. Wenn darüber hinaus ohne einen synchronisierten Block ein Array zB zugreift, kann ein anderer Thread nicht changement dafür ... tatsächlich ein anderer Thread wird nicht irgendwelche Change dafür , wenn es bereits eine Kopie des Arrays in der x-Level - Cache hat ( aka (1st / 2nd / 3rd-Level-Caches) des Thread-Handling-CPU-Kerns.

Synchronisierte Blöcke sind jedoch nur eine Seite der Medaille: Wenn Sie tatsächlich von einem nicht synchronisierten Kontext aus auf ein Objekt in einem synchronisierten Kontext zugreifen, wird das Objekt auch innerhalb eines synchronisierten Blocks nicht synchronisiert, da es eine eigene Kopie von enthält Objekt in seinem Cache. Ich habe hier über diese Probleme geschrieben: https://stackoverflow.com/a/21462631 und Wenn eine Sperre ein nicht endgültiges Objekt enthält, kann die Objektreferenz noch von einem anderen Thread geändert werden?

Darüber hinaus bin ich überzeugt, dass die x-Level-Caches für die meisten nicht reproduzierbaren Laufzeitfehler verantwortlich sind. Das liegt daran, dass die Entwickler in der Regel nicht lernen, wie die CPU funktioniert oder wie sich die Speicherhierarchie auf die Ausführung von Anwendungen auswirkt: http://en.wikipedia.org/wiki/Memory_hierarchy

Es bleibt ein Rätsel, warum Programmierklassen nicht zuerst mit der Speicherhierarchie und der CPU-Architektur beginnen. "Hallo Welt" wird hier nicht helfen. ;)



Hmm .. nicht sicher, ob ich folge. Wenn das Zwischenspeichern der einzige Grund war, Wartezeit und Benachrichtigung synchronisiert zu setzen, warum wird die Synchronisierung nicht in die Implementierung von Wartezeit / Benachrichtigung integriert?
Aioobe

Gute Frage, da Warten / Benachrichtigen sehr gut synchronisierte Methoden sein könnten ... Vielleicht kennen die ehemaligen Java-Entwickler von Sun die Antwort? Werfen Sie einen Blick in den obigen Link, oder vielleicht hilft Ihnen dies auch: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Ein Grund könnte sein: In den frühen Tagen von Java gab es keine Kompilierungsfehler, wenn nicht synchronisiert aufgerufen wurde, bevor diese Multithreading-Vorgänge ausgeführt wurden. Stattdessen gab es nur Laufzeitfehler (zB coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Vielleicht haben sie @SUN wirklich gedacht, dass Programmierer kontaktiert werden, wenn sie diese Fehler erhalten, was ihnen möglicherweise die Möglichkeit gegeben hat, mehr von ihren Servern zu verkaufen. Wann hat sich das geändert? Vielleicht Java 5.0 oder 6.0, aber eigentlich erinnere ich mich nicht daran, ehrlich zu sein ...
Marcus

TBH Ich sehe ein paar Probleme mit Ihrer Antwort. 1) Ihr zweiter Satz macht keinen Sinn: Es spielt keine Rolle, für welches Objekt ein Thread gesperrt ist. Unabhängig davon, auf welchem ​​Objekt zwei Threads synchronisiert werden, werden alle Änderungen sichtbar gemacht. 2) Sie sagen, ein anderer Thread "wird keine Änderungen sehen". Dies sollte "darf nicht" sein . 3) Ich weiß nicht, warum Sie Caches der 1., 2. und 3. Ebene aufrufen ... Was hier zählt, ist, was das Java-Speichermodell sagt und was in JLS angegeben ist. Während die Hardwarearchitektur hilfreich sein kann, um zu verstehen, warum JLS sagt, was sie tut, ist sie in diesem Zusammenhang streng genommen irrelevant.
Aioobe

0

direkt aus diesem Java Orakel Tutorial:

Wenn ein Thread d.wait aufruft, muss er die intrinsische Sperre für d besitzen - andernfalls wird ein Fehler ausgelöst. Das Aufrufen von wait innerhalb einer synchronisierten Methode ist eine einfache Möglichkeit, die intrinsische Sperre zu erhalten.


Aus der vom Autor gestellten Frage geht nicht hervor, dass der Autor der Frage ein klares Verständnis dafür hat, was ich aus dem Tutorial zitiert habe. Außerdem erklärt meine Antwort "Warum".
Rollerball

0

Wenn Sie notify () von einem Objekt t aus aufrufen, benachrichtigt Java eine bestimmte t.wait () -Methode. Aber wie sucht und benachrichtigt Java eine bestimmte Wartemethode?

Java untersucht nur den synchronisierten Codeblock, der durch das Objekt t gesperrt wurde. Java kann nicht den gesamten Code durchsuchen, um ein bestimmtes t.wait () zu benachrichtigen.


0

gemäß Dokumentation:

Der aktuelle Thread muss den Monitor dieses Objekts besitzen. Der Thread gibt den Besitz dieses Monitors frei.

wait()Methode bedeutet einfach, dass die Sperre für das Objekt aufgehoben wird. Das Objekt wird also nur innerhalb des synchronisierten Blocks / der synchronisierten Methode gesperrt. Wenn sich der Thread außerhalb des Synchronisierungsblocks befindet, bedeutet dies, dass er nicht gesperrt ist. Wenn er nicht gesperrt ist, was würden Sie dann für das Objekt freigeben?


0

Thread wartet auf das Überwachungsobjekt (Objekt, das vom Synchronisationsblock verwendet wird). Es kann n Anzahl von Überwachungsobjekten auf der gesamten Reise eines einzelnen Threads geben. Wenn der Thread außerhalb des Synchronisationsblocks wartet, gibt es kein Überwachungsobjekt und auch keinen anderen Thread, der für den Zugriff auf das Überwachungsobjekt benachrichtigt wird. Wie würde der Thread außerhalb des Synchronisationsblocks wissen, dass er benachrichtigt wurde? Dies ist auch einer der Gründe, warum wait (), notify () und notifyAll () eher in der Objektklasse als in der Thread-Klasse liegen.

Grundsätzlich ist das Überwachungsobjekt hier eine gemeinsame Ressource für alle Threads, und Überwachungsobjekte können nur im Synchronisationsblock verfügbar sein.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.