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:
Ein Consumer-Thread ruft auf take()
und sieht, dass die buffer.isEmpty()
.
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();
Der Consumer-Thread ruft jetzt auf wait()
(und übersieht den notify()
gerade aufgerufenen).
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 synchronized
diese Option , um sicherzustellen, dass notify
niemals zwischen isEmpty
und 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 synchronized
Blöcken ausgeführt wird.
Dieser Beitrag wurde hier als Artikel umgeschrieben: Java: Warum warten muss in einem synchronisierten Block aufgerufen werden