Das Ziel der Thread-Kommunikation besteht darin, Threads in die Lage zu versetzen, einander Signale zu senden. Thread-Kommunikation hingegen ermöglicht es Threads, auf Signale von anderen Threads zu warten.
Kommunikation über gemeinsames Objekt
Beschäftigtes Warten
wait(), notify() und notifyAll()
Signal verloren
Falsch Wakeup
Mehrere Threads warten auf das gleiche Signal
Rufen Sie wait() nicht für konstante Zeichenfolgen oder globale Objekte auf
Kommunikation über gemeinsam genutzte Objekte
Eine einfache Möglichkeit, Signale zwischen Threads zu senden, besteht darin, den Signalwert in einer Variablen eines gemeinsam genutzten Objekts festzulegen. Thread A setzt die boolesche Mitgliedsvariable hasDataToProcess in einem synchronisierten Block auf true, und Thread B liest auch die Mitgliedsvariable hasDataToProcess im synchronisierten Block. In diesem einfachen Beispiel wird ein Objekt verwendet, das ein Signal enthält und Set- und Prüfmethoden bereitstellt:
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
Threads A und B müssen einen Verweis auf eine gemeinsam genutzte Instanz von MySignal erhalten, um kommunizieren zu können. Wenn sie Verweise auf verschiedene MySingal-Instanzen enthalten, können sie das Signal des anderen nicht erkennen. Die zu verarbeitenden Daten können in einem gemeinsamen Cache gespeichert werden, der getrennt von der MySignal-Instanz gespeichert wird.
Besetztes Warten
Thread B, der sich auf die Datenverarbeitung vorbereitet, wartet darauf, dass die Daten verfügbar werden. Mit anderen Worten: Es wartet auf ein Signal von Thread A, das dazu führt, dass hasDataToProcess() „true“ zurückgibt. Thread B läuft in einer Schleife und wartet auf dieses Signal:
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
wait(), notify() und notifyAll()
Busy Waiting hat keine Auswirkungen auf die CPU, die das Warten ausführt Thread-Auslastung, es sei denn, die durchschnittliche Wartezeit ist sehr kurz. Andernfalls ist es klüger, den wartenden Thread schlafen zu lassen oder sich in einem nicht ausgeführten Zustand zu befinden, bis er das Signal empfängt, auf das er wartet.
Java verfügt über einen integrierten Wartemechanismus, der es Threads ermöglicht, nicht mehr ausgeführt zu werden, während sie auf ein Signal warten. Die Klasse java.lang.Object definiert drei Methoden, wait(), notify() und notifyAll(), um diesen Wartemechanismus zu implementieren.
Sobald ein Thread die Methode wait() eines beliebigen Objekts aufruft, wird er nicht ausgeführt, bis ein anderer Thread die Methode notify() desselben Objekts aufruft. Um wait() oder notify() aufzurufen, muss der Thread zunächst die Sperre für dieses Objekt erwerben. Mit anderen Worten, der Thread muss im synchronisierten Block wait() oder notify() aufrufen. Das Folgende ist eine modifizierte Version von MySingal – MyWaitNotify mit wait() und notify():
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
Der wartende Thread ruft doWait() auf und der wache Thread ruft doNotify() auf. Wenn ein Thread die notify()-Methode eines Objekts aufruft, wird ein Thread unter allen Threads, die auf das Objekt warten, aktiviert und darf ausgeführt werden (Hinweis: Der Thread, der aktiviert wird, ist zufällig und Sie können nicht angeben, welcher Thread aktiviert werden soll.) . Es bietet außerdem eine notifyAll()-Methode, um alle Threads zu aktivieren, die auf ein bestimmtes Objekt warten.
Wie Sie sehen können, rufen sowohl der wartende Thread als auch der wachende Thread wait() und notify() im synchronisierten Block auf. Dies ist Pflicht! Ein Thread kann wait(), notify() oder notifyAll() nicht aufrufen, wenn er die Objektsperre nicht hält. Andernfalls wird eine IllegalMonitorStateException-Ausnahme ausgelöst.
(Hinweis: So wird die JVM implementiert. Wenn Sie wait aufrufen, prüft sie zunächst, ob der aktuelle Thread der Besitzer der Sperre ist. Wenn nicht, löst sie ein IllegalMonitorStateExcept aus.)
Aber das Wie ist das möglich? Wenn der wartende Thread im synchronisierten Block ausgeführt wird, hält er dann nicht immer die Sperre des Monitorobjekts (myMonitor-Objekt)? Kann der wartende Thread den aktiven Thread nicht daran hindern, in den synchronisierten Block von doNotify() einzutreten? Die Antwort lautet: Tatsächlich nicht. Sobald ein Thread die Methode wait() aufruft, gibt er die Sperre frei, die er für das Monitorobjekt hält. Dadurch können auch andere Threads wait() oder notify() aufrufen.
Sobald ein Thread aktiviert ist, kann er den Aufruf der Methode „wait()“ nicht sofort beenden, bis der
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } } <br>
Thread, der notify() aufruft, seinen eigenen Synchronisierungsblock verlässt. Mit anderen Worten: Der aufgeweckte Thread muss die Sperre des Monitorobjekts erneut erlangen, bevor er den Aufruf der Methode „wait()“ beenden kann, da der Aufruf der Methode „wait“ in einem synchronisierten Block ausgeführt wird. Wenn mehrere Threads durch notifyAll() geweckt werden, kann nur ein Thread gleichzeitig die Methode wait() verlassen, da jeder Thread die Sperre des Monitorobjekts erhalten muss, bevor er wait() beendet.
Verpasste Signale
Die Methoden notify() und notifyAll() speichern nicht die Methode, die sie aufgerufen hat, da es beim Aufruf dieser beiden Methoden möglich ist, dass sich kein Thread im Wartezustand befindet . Das Benachrichtigungssignal wird nach Ablauf verworfen. Wenn daher ein Thread notify() aufruft, bevor der benachrichtigte Thread wait() aufruft, verpasst der wartende Thread das Signal. Dies kann ein Problem sein oder auch nicht. In einigen Fällen kann dies jedoch dazu führen, dass der wartende Thread ewig wartet und nie aufwacht, weil der Thread das Wecksignal verpasst hat.
Um Signalverluste zu vermeiden, müssen diese in der Signalklasse gespeichert werden. Im MyWaitNotify-Beispiel sollte das Benachrichtigungssignal in einer Mitgliedsvariablen der MyWaitNotify-Instanz gespeichert werden. Hier ist eine modifizierte Version von MyWaitNotify:
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 doNotify()方法在调用 notify()前把 wasSignalled 变量设为 true。同时,留意 doWait()方法在调用 wait()前会检查 wasSignalled 变量。事实上,如果没有信号在前一次 doWait()调用和这次 doWait()调用之间的时间段里被接收到,它将只调用 wait()。
(校注:为了避免信号丢失, 用一个变量来保存是否被通知过。在 notify 前,设置自己已经被通知过。在 wait 后,设置自己没有被通知过,需要等待通知。)
假唤醒
由于莫名其妙的原因,线程有可能在没有调用过 notify()和 notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。
如果在 MyWaitNotify2 的 doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。
为了防止假唤醒,保存信号的成员变量将在一个 while 循环里接受检查,而不是在 if 表达式里。这样的一个 while 循环叫做自旋锁(校注:这种做法要慎重,目前的 JVM 实现自旋会消耗 CPU,如果长时间不调用 doNotify 方法,doWait 方法会一直自旋,CPU 会消耗太大)。被唤醒的线程会自旋直到自旋锁(while 循环)里的条件变为 false。以下 MyWaitNotify2 的修改版本展示了这点:
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 wait()方法是在 while 循环里,而不在 if 表达式里。如果等待线程没有收到信号就唤醒,wasSignalled 变量将变为 false,while 循环会再执行一次,促使醒来的线程回到等待状态。
多个线程等待相同信号
如果你有多个线程在等待,被 notifyAll()唤醒,但只有一个被允许继续执行,使用 while 循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出 wait()调用并清除 wasSignalled 标志(设为 false)。一旦这个线程退出 doWait()的同步块,其他线程退出 wait()调用,并在 while 循环里检查 wasSignalled 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。
不要在字符串常量或全局对象中调用 wait()
(校注:本章说的字符串常量指的是值为常量的变量)
本文早期的一个版本在 MyWaitNotify 例子里使用字符串常量(””)作为管程对象。以下是那个例子:
public class MyWaitNotify{ String myMonitorObject = ""; boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
在空字符串作为锁的同步块(或者其他常量字符串)里调用 wait()和 notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有 2 个不同的 MyWaitNotify 实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个 MyWaitNotify 实例上调用 doWait()的线程会被在第二个 MyWaitNotify 实例上调用 doNotify()的线程唤醒。这种情况可以画成以下这张图:
起初这可能不像个大问题。毕竟,如果 doNotify()在第二个 MyWaitNotify 实例上被调用,真正发生的事不外乎线程 A 和 B 被错误的唤醒了 。这个被唤醒的线程(A 或者 B)将在 while 循环里检查信号值,然后回到等待状态,因为 doNotify()并没有在第一个 MyWaitNotify 实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程 A 或者 B 在信号值没有更新的情况下唤醒。但是代码处理了这种情况,所以线程回到了等待状态。记住,即使 4 个线程在相同的共享字符串实例上调用 wait()和 notify(),doWait()和 doNotify()里的信号还会被 2 个 MyWaitNotify 实例分别保存。在 MyWaitNotify1 上的一次 doNotify()调用可能唤醒 MyWaitNotify2 的线程,但是信号值只会保存在 MyWaitNotify1 里。
问题在于,由于 doNotify()仅调用了 notify()而不是 notifyAll(),即使有 4 个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程 A 或 B 被发给 C 或 D 的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而 C 和 D 都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C 和 D 被发送过信号,只是都不能对信号作出回应。
Wenn die doNotify()-Methode notifyAll() anstelle von notify() aufruft, werden alle wartenden Threads geweckt und überprüfen der Reihe nach den Signalwert. Die Threads A und B kehren in den Wartezustand zurück, aber nur ein Thread, C oder D, bemerkt das Signal und verlässt den Methodenaufruf doWait(). Der andere in C oder D kehrt in den Wartezustand zurück, da der Thread, der das Signal erhalten hat, beim Verlassen von doWait() den Signalwert gelöscht (auf false gesetzt) hat.
Nachdem Sie den obigen Absatz gelesen haben, könnten Sie versuchen, notifyAll() anstelle von notify() zu verwenden, aber das ist im Hinblick auf die Leistung eine schlechte Idee. Es gibt keinen Grund, jedes Mal alle Threads aufzuwecken, wenn nur ein Thread auf das Signal reagieren kann.
Also: Verwenden Sie im wait()/notify()-Mechanismus keine globalen Objekte, String-Konstanten usw. Es sollten entsprechende Unikate verwendet werden. Anstatt beispielsweise wait()/notify() für eine leere Zeichenfolge aufzurufen, verfügt jede Instanz von MyWaitNotify3 über ein eigenes Monitorobjekt.
Das Obige ist die Sammlung von Informationen über Java-Multithreading und Thread-Kommunikation. Wir werden in Zukunft weiterhin relevante Informationen hinzufügen. Vielen Dank für Ihre Unterstützung dieser Website!
Weitere Java-Multithreading-Thread-Kommunikationsbeispiele und verwandte Artikel finden Sie auf der chinesischen PHP-Website!