11.7.1 | Synchronisation mit Monitoren |
Das Konzept des Monitors wurde von C.A.R. Hoare eingeführt und passt von der Konstruktion her sehr gut zur objektorientierten Programmierung. Unter einem Monitor kann man sich ein Objekt vorstellen, das bestimmte Attribute in sich einschließt, auf die nur über definierte Zugangsmethoden zugegriffen werden kann, wobei immer nur eine Zugangsmethode zur gleichen Zeit aktiv sein kann. Der Monitor ist besetzt, sobald eine Zugangsmethode ausgeführt wird. Zu jeder Zugangsmethode verwaltet der Monitor eine Warteschlange, in die Threads eingereiht werden, die eine solche Methode aufrufen, während der Monitor besetzt ist.
Um einen Monitor in Java zu verwenden, fügt man den Modifier synchronized in der Deklaration der Methoden ein, die Zugangsmethoden des Monitors sein sollen. Die von dieser Klasse erzeugten Objekte kann man damit als Monitore betrachten, bei denen die synchronized-Methoden Zugangsmethoden sind. Sobald eine solche Methode von einem Thread ausgeführt wird, kann kein anderer Thread eine Zugangsmethode dieses Objekts ausführen. Im Beispiel der Bank könnte man die Methode überweisung() mit dem Schlüsselwort synchronized versehen. Damit würde man das Programm künstlich sequenzialisieren und nur eine Überweisung in einer gegebenen Bank zur gleichen Zeit zulassen.
Dieses Vorgehen ist natürlich nicht gerade elegant. Wenn die einzelnen Überweisungen ohnehin nacheinander ablaufen, so könnte man sich das Starten paralleler Threads gleich sparen. Als Verbesserung könnte man sich überlegen, die einzelnen Konten als Objekte zu implementieren, die eine Subtraktions- und Additionsmethode anbieten. Die Bank wäre dann ein Objekt, das die Konten in sich einschließt und bei einer Überweisung die Subtraktions- und Additionsmethoden seiner Konten aufruft. Diese Methoden würden dann das Schlüsselwort synchronized enthalten, und so wäre ausgeschlossen, dass während einer Subtraktion auf den ungültigen Kontostand zugegriffen wird.
Die Realisierung der Klasse Konto hätte somit folgende Gestalt:class Konto { int stand; public Konto(int betrag) { stand = betrag; } public synchronized void add(int betrag) { int neuerBetrag; neuerBetrag = stand; neuerBetrag += betrag; stand = neuerBetrag; } public synchronized void sub(int betrag) { int neuerBetrag; neuerBetrag = stand; neuerBetrag -= betrag; stand = neuerBetrag; } public synchronized int kontoStand() { return stand; } }Nach dieser Änderung verlaufen die Überweisungen ordnungsgemäß:Vorher: Konto 0: 30 Konto 1: 50 Konto 2: 100 Nachher: Konto 0: 30 Konto 1: 50 Konto 2: 100Die Synchronisation zweier oder mehrerer Threads birgt jedoch auch neue Gefahren in sich. Zur Verdeutlichung stelle man sich folgendes Beispiel vor: Mehrere Threads benötigen je zwei Ressourcen gleichzeitig, um ihre Arbeit zu verrichten. In der Grundlagenliteratur verwendet man hier meist das Beispiel der denkenden und essenden Philosophen. Mehrere Philosophen verbringen ihre Zeit mit Denken und Essen. Sie sitzen an einem runden Tisch, jeder Philosoph hat einen Teller vor sich, und zwischen den Tellern liegt eine Gabel.Wenn ein Philosoph essen will, benötigt er zwei Gabeln. Da er sich jedoch beide mit seinen Nachbarn teilen muss, können nie zwei benachbart sitzende Philosophen gleichzeitig essen.
Während die Philosophen denken, sind sie ausschließlich mit sich selbst beschäftigt und stehen nicht in Kontakt mit ihrer Umwelt. Wenn sie jedoch vom vielen Denken hungrig geworden sind, müssen sie versuchen, zwei Gabeln zu bekommen, um dann zu essen.
Damit das Essen gerecht verteilt wird und kein Philosoph verhungert, muss man sich eine Regelung für die Vergabe des Bestecks einfallen lassen. Man könnte auf die Idee kommen, dass jeder Philosoph zunächst das Besteck zu seiner Rechten ergreift, dieses also in Besitz nimmt, und dann darauf wartet, dass das entsprechende andere Teil zu seiner Linken frei ist. Wenn dabei zufällig alle Philosophen gleichzeitig hungrig werden, dann nimmt jeder das Teil zu seiner Rechten in Besitz. Damit hält jeder eine Gabel, und auf dem Tisch ist kein Besteck mehr übrig. Alle Philosophen warten gegenseitig darauf, dass ein anderer sein Besteck wieder ablegt. In diesem Zustand würden alle Philosophen ewig warten und so verhungern. Man nennt diesen Zustand Deadlock.
Auf ein Java-Programm übertragen könnte ein Deadlock zustande kommen, wenn sich zwei Monitore in den Zugangsmethoden gegenseitig aufrufen können. In diesem Fall kann es vorkommen, dass ein Thread den Monitor A betritt und ein anderer den Monitor B. Wenn nun in der Zugangsmethode von A eine Zugangsmethode von B aufgerufen wird und in B eine von A, so wartet der erste Thread darauf, dass der zweite den Monitor B verlässt, dieser wartet jedoch darauf, dass der erste den Monitor A verlässt.
Um derartige Zustände zu vermeiden, sollten inhaltlich zusammengehörige Funktionen, die synchronisiert ablaufen müssen, auch in einem einzigen Monitor abgehandelt werden. Aufrufe von Zugangsmethoden aus anderen Monitoren heraus sind generell gefährlich.
Ein weiteres Problem im Zusammenhang mit Monitoren ist die Methode Thread.stop(). Der stop()-Aufruf bewirkt, dass der angesprochene Thread sofort alle Monitore freigibt, die er besitzt, und zwar auch dann, wenn er eine Zugangsmethode noch nicht bis zum Ende ausgeführt hat. Dadurch kann es passieren, dass ein gestoppter Thread einen Monitor in einem inkonsistenten Zustand verlässt, was beim nächsten Thread, der den Monitor betritt, zu großen Problemen führen kann. Weil dieser Problematik kaum Abhilfe zu schaffen ist, wurde Thread.stop() seit Version 1.2 verworfen.
Dasselbe Schicksal ereilte die Methoden Thread.suspend() und Thread.resume(), weil ein Thread beim Suspendieren alle Monitore behält, die er besitzt. Wenn der Thread, der den resume()-Aufruf ausführt, zuvor den Monitor betreten muss, den der suspendierte Thread besitzt, resultiert ein Deadlock.
Der Vollständigkeit halber sei an dieser Stelle noch der Modifier volatile erwähnt, mit dem Datenelemente versehen werden sollten, auf die unsynchronisiert zugegriffen wird.