|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Java Multithread Support - Details of synchronized
|
||||||||||||||||||
Um Klassen threadsicher zu machen, kann man Methoden der Klassen
als
synchronized
deklarieren. Die Qualifizierung mit
synchronized
bewirkt, dass zuerst eine begonnene
synchronized
Methode zu Ende
ausgeführt wird, bevor eine neue
synchronized
Methode in
einem anderen Thread begonnen werden kann. Das heißt,
synchronized
Methoden können nicht konkurrierend zueinander ablaufen, sondern
werden immer sequentiell nacheinander ausgeführt. In dieser
Ausgabe unserer Kolumne werden wir uns die Details von
synchronized
genauer ansehen.
Threadsichere KlassenIn unserem letzten Artikel haben wir eine Integer-Stack-Klasse so implementiert, dass mehrere konkurrierende Threads problemlos gemeinsam auf eine Instanz dieser Klasse zugreifen können. Diese Eigenschaft der Klasse haben wir threadsicher (englisch: thread safe) genannt. Hier ist noch einmal die Implementierung der Klasse:public class IntStack {Wir wollten diesen IntStack threadsicher machen. Zu diesem Zweck haben wir uns angeschaut, welche Methoden der Klasse zu Problemen führen können, wenn sie in zwei verschiedenen Threads zum gleichen Zeitpunkt aufgerufen werden. Diese Methoden haben wir dann in der Implementierung als synchronized deklariert. Zu synchronized haben wir bisher gesagt, dass sich die synchronized Methoden eines Objekts untereinander atomar verhalten. Das heißt, es ist garantiert, dass erst eine begonnene synchronized Methode zu Ende ausgeführt wird, bevor eine neue synchronized Methode in einem anderen Thread begonnen werden kann. Sehen wir uns nun genauer an, wie synchronized funktioniert. MutexIn jeder Multithread-Ablaufumgebung gibt es eine Art von Abstraktion, die man Mutex nennt. Der Name Mutex steht für mutual exclusive lock (gegenseitig ausschließende Sperre). Der Name lässt es schon vermuten: ein Mutex ist eine Sperre, die genau einem Thread Durchgang gewährt und alle anderen Threads aufhält. Der Thread, der durchgelassen wird, ist der Thread, der als erster die Sperre erreicht hat. Man spricht dann auch davon, dass dieser Thread die Sperre hält (englisch: holds the lock) oder dass ihm das Mutex gehört (englisch: owns the mutex). In Java sind die Mutex-Instanzen der Ablaufumgebung nicht explizit sichtbar oder zugreifbar. Es ist vielmehr so, dass jedes Java-Objekt unabhängig vom konkreten Typ ein implizites Mutex enthält. Mit dem synchronized Keyword kann man nun ein solches Mutex sperren und entsperren:...Syntaktisch sieht das so aus, dass nach dem synchronized Keyword in runden Klammern das Objekt genannt ist, dessen Mutex genutzt wird. Danach kommt ein Code-Block, wie in Java üblich mit einer öffnenden geschweiften Klammer beginnend und mit einer schließenden geschweiften Klammer endend. Mit Beginn des Code-Blocks wird die Sperre des Mutex aktiv und mit Ende des Code-Blocks wieder inaktiv. Auch wenn der Code-Block auf Grund einer Exception verlassen wird, wird die Sperre des Mutex zu diesem Zeitpunkt aufgehoben. Bei der Nutzung von synchronized gibt es in Java gibt es keine andere Möglichkeit, die Sperre des Mutex zu aktivieren oder deaktivieren, obwohl das auf Grund der JVM, die dafür explizite Operationscodes hat, durchaus möglich wäre. Die Benutzung des Mutex ist in Java so starr an die Blockgrenzen gebunden, weil auf diese Weise sichergestellt ist, dass nicht versehentlich vergessen werden kann, die Sperre eines Mutex wieder freizugegeben. Durch die Kopplung des Sperrens und Freigebens an Blockgrenzen ist gesichert, dass das Lock auf jeden Fall deaktiviert wird, das bedeutet jedoch nicht, dass das Lock immer zum richtigen (d.h. zum frühest möglichen) Zeitpunkt freigegeben wird. Die starre Kopplung an Blockgrenzen bringt darüber hinaus funktionale Einschränkungen mit sich: so ist es zum Beispiel nicht möglich, eine zweite Sperre nach einer ersten Sperre so zu aktivieren, dass diese nach der ersten deaktiviert wird. Einen solchen Vorgang nennt man Hand-Over-Hand Locking (siehe Abb. 1). Das geht in Java nicht; es gibt nur ein geschachteltes Locking (Nested Locking, siehe Abb .2), das den geschachtelten Blockstrukturen entspricht. Abbildung 1: Hand-Over-Hand Locking
Mit dem JDK 1.5 gibt es an solchen Stellen mehr Flexibilität in der Programmierung. Im nächsten Artikel werden wir diese neuen Möglichkeiten diskutieren. In diesem Artikel beschränken wir uns auf die pre-JDK 1.5 Funktionalität. synchronized Instanzmethoden und synchronized BlöckeWas hat nun der synchronized-Block mit einer synchronized-Instanzmethode zu tun? Eigentlich ist die synchronized-Instanzmethode nur eine verkürzte Schreibweise dafür, dass der Code-Block der gesamten Methode synchronized ist, und zwar mit dem Mutex, das mit this assoziiert ist. Das heißt, die Sperre des Mutex von this wird während der gesamten Laufzeit der Methode gehalten. Die beiden folgenden Methodendefinitionen sind also semantisch gleichwertig:synchronized public int pop () {Den Code-Abschnitt, in dem eine Sperre aktiv ist oder aktiv sein sollte, nennt man übrigens kritischen Bereich (englisch: critical region). Für eine gute Programm-Performance ist es wichtig, kritische Bereiche so klein wie möglich zu wählen und damit eine hohe Parallelität des Programms zu gewährleisten. Weil nicht nur ganze Methoden, sondern auch einzelne Code-Blöcke synchronized definieren werden können, kann ein kritischer Bereich relativ fein granular gewählt werden. Natürlich muss darauf geachtet werden, dass der kritische Bereich immer noch groß genug gewählt wird, um einen effektiven Schutz durch die Sperre des Mutex zu haben. Schauen wir uns noch einmal die obige pop-Methode daraufhin an, ob man bei ihr den kritischen Bereich verkleinern kann. Aus dem vorhergehenden Artikel wissen wir, dass die für die Thread-Sicherheit relevanten Elemente hier der Zugriff auf den Stackpointer cnt und das Array array, in dem die Werte gespeichert werden, sind. Eine Einschränkung ist, wie oben bereits diskutiert, dass wir einen kritischen Bereich in Java (vor JDK 1.5) nur als Code-Block definieren können. Unter dieser Randbedingung ist das Sperren der ganzen Methode der kleinste mögliche kritische Bereich: die Abfrage der Bedingung (cnt > 0) und der if-Zweig (return(array[--cnt]);) müssen zusammen in einem kritischen Bereich sein. Im else-Zweig wird gar nicht auf cnt und array zugegriffen und es ist damit völlig überflüssig, hier noch die Sperre durch das Mutex weiter aufrechtzuerhalten. Aber da die Sperre an Blockgrenzen gebunden ist, geht es nicht anders. Details der Mutex SperreWie sieht es eigentlich aus, wenn wir die vorhandene push-Methodesynchronized public void push (int elm) {folgendermaßen ändern: synchronized public void push (int elm) {wobei capacity() so aussieht wie bisher definiert: public int capacity() { return (array.length); }Besteht die Sperre des Mutex auch, wenn capacity() aufgerufen wird, obwohl diese Methode nicht synchronized ist? Oder ist das ein Problem, wenn eine nicht-synchronized Methode in einer synchronized Methode aufgerufen wird? Nein, es ist kein Problem, denn der synchronized-Block wird nicht verlassen. Das Mutex bleibt auch beim Aufruf von capacity() aus einem synchronized-Block heraus gesperrt. Verallgemeinert bedeutet dies, dass das Mutex eines synchronized Blocks weiterhin gesperrt ist, auch wenn aus diesem synchronized Block heraus Methoden aufgerufen werden, die nicht synchronized deklariert sind. Schauen wir uns eine andere Situation an. Nehmen wir an, dass wir unseren IntStack um eine Methode erweitern, die genau dann eine Exception (ValueZeroException) wirft, falls der Wert, der beim pop() zurückkommen würde, 0 ist. Wir implementieren die Methode folgendermaßen: synchronized public int popIfNotZero() thows ValueZeroException {Die Methoden peek() und pop() sind die am Anfang des Artikels definierten. Damit sich der oberste Wert des Stacks zwischen der Abfrage durch peek() und dem Herausholen durch pop() nicht ändern kann, müssen die Aufrufe von peek() und pop() beide in einem kritischen Bereich liegen. Der kleinste Block, der diesen kritischen Bereich umgibt, ist die gesamte Methode. Deshalb ist popIfNotZero() als synchronized deklariert. Was geschieht nun, wenn der Thread innerhalb von popIfNotZero() die Methode peek() aufruft? Er hält bereits die Sperre des Mutex, da diese aktiviert wurde, als er den Funktionsblock von popIfNotZero() betreten hat. Andererseits ist die Methode peek() aber auch synchronized, d.h. die Sperre des Mutex soll erneut aktiviert werden. Ist das ein Problem? Nein. Die Sperre eines Mutex (und wir sprechen hier immer über ein Mutex, nämlich das von this) kann rekursiv beliebig oft vom selben Thread aktiviert werden. Würde es sich aber um ein anderes Mutex handeln, so bestünde potentiell die Gefahr eines Deadlocks. Diese Problematik werden wir in einem zukünftigen Artikel behandeln. In der Java Language Specification (/JLS/) ist noch explizit erwähnt, dass die Sperre erst dann inaktiv wird und einen anderen Thread durchläßt, wenn sie genauso häufig deaktiviert wurde, wie sie vorher aktiviert wurde. Bei der Benutzung von synchronized spielt diese Anforderung aber keine Rolle, da Aktivieren und Deaktivieren nur am Blockanfang bzw. -ende desselben Blocks erfolgen kann und so sichergestellt ist, dass die Anzahl gleich ist. Darüber hinaus kann eine Mutex-Sperre Sperre nur von dem Thread deaktiviert werden kann, der sie vorher aktiviert hat. Auch diese Einschränkung ist lediglich von theoretischer Bedeutung ist, da das Aufheben der Sperre nur implizit am Ende des synchronized-Block erfolgen kann. Anfang und Ende eines Blocks liegen natürlich immer im selben Thread. MonitorBis jetzt haben wir uns Situationen angesehen, bei denen das Mutex, das mit this assoziiert ist, verwendet wurde, um die Instanzmethoden einer Klasse threadsicher zu machen. Das ist der Standardfall in Java. Er ergibt sich daraus, dass synchronized Instanzmethoden automatisch das Mutex von this verwenden. Hat man in einer Klasse zusätzlich noch Methoden, bei denen man nicht die ganze Methode sperren muss, sondern nur einzelne Bereiche, so muss man bei der Definition der entsprechenden synchronized-Blöcke das this explizit verwenden, damit für die synchronized-Blöcke dieselbe Sperre wie bei den synchronized Methoden verwendet wird.In der prozeduralen Systemprogrammierung gibt es den Begriff des Monitors. Er beschreibt ein Muster (englisch: Pattern), bei dem die Zugriffe auf eine Ressource dadurch threadsicher gemacht werden, dass alle Zugriffe über Zugriffsmethoden erfolgen, die sich über ein gemeinsames Mutex synchronisieren. Die Designer von Java haben den Begriff Monitor für das Konzept einer Klasse übernommen, deren Instanzen threadsicher sind. Die Ressource besteht dabei aus den Instanzvariablen und die Zugriffsmethoden auf die Ressource sind die Instanzmethoden der Klasse. Wegen dieser Analogie findet sich in der JavaDoc des JDK häufig der Begriff monitor lock. Damit ist dann das mit dem Objekt assoziierte Mutex gemeint. Im Standardfall der Synchronisierung über this ist ein Monitor Lock gleichzeitig auch das Mutex, das für die Threadsicherheit des Objekts sorgt. Klassenvariablen und KlassenmethodenKlassenvariablen (oder statische Variablen, wie sie auch genannt werden) sind automatisch immer von allen Theads einer JVM zugreifbar. Dies ist etwas anders als bei Instanzvariablen, bei denen erst einmal die Referenz der Instanz in einem Thread bekannt sein muss, damit darauf zugegriffen werden kann. Der Zugriff auf Klassenvariablen ist natürlich zusätzlich noch durch die Zugriffsrechte (englisch: access modifiers) wie private, protected, usw. eingeschränkt. Aber auch auf eine private Klassenvariable kann natürlich recht einfach aus verschiedenen Threads gleichzeitig zugegriffen werden. Dazu muss lediglich aus zwei verschiedene Instanzen, die jeweils in einem der Threads im Zugriff sind, auf die Klassenvariable zugegriffen werden. Klassenvariablen sind also implizit gemeinsam verwendete Variablen, so dass der konkurrierende Zugriff aus mehreren Threads heraus, synchronisiert werden muß.Dabei ist der Zugriff auf Klassenvariablen vom Typ long und double ein wenig problematisch, weil nicht auf Anhieb klar ist, ob und wie man den Zugriff synchronisieren muss. Im letzten Artikel haben wir erwähnt, dass der Zugriff auf long und double Variablen nicht atomar ist. Wenn eine Klasse non-final Klassenvariablen dieser beiden Typen hat, dann kann von mehreren Threads konkurrierend auf diese Klassenvariablen zugegriffen werden. Da der Zugriff nicht atomar ist, kann er potentiell zu Fehlern führen kann. Diese Fehler können vermieden werden, indem Klassenvariablen vom Typ long und double als volatile deklariert werden. Der Zugriff auf volatile Variablen ist nämlich atomar. Leider wird die Lösung mit Hilfe von volatile von verschiedenen älteren JVMs nicht unterstützt; der Zugriff auf volatile Variablen vom Typ long und double ist in diesen JVMs nicht atomar, obwohl er es sein sollte. Darüber hinaus bedarf das Keyword volatile grundsätzlich einer etwas längeren Diskussion, die wir erst in einem zukünftigen Artikel führen möchten. Deshalb wollen wir uns an dieser Stelle eine Lösung ansehen, die ohne volatile auskommt. Sie besteht darin, dass wir den lesenden und schreibenden Zugriff auf Klassenvariablen über Klassenmethoden theadsicher machen Zur Illustration erweitern wir die Funktionalität unseres IntStacks so, dass er die Anzahl der eigenen Instanzen zählt, d.h. die Zahl der existierenden und noch nicht vom Garbage Collector finalisierten Objekte. Dabei soll „finalisiert“ heißen, dass die finalize() Methode noch nicht aufgerufen wurde. Der Zähler soll vom Typ long sein. Dazu wird man die Implementierung von IntStack folgendermaßen erweitern bzw. verändern: public class IntStack {Die Zugriffsmethoden incInstance(), decInstance() und numberOfInstances() sind synchronized deklariert worden; andernfalls wären die nicht-atomaren Zugriffe auf instanceCnt ungeschützt. Wie wir bereits gesehen haben, wird bei synchronized Instanzmethoden das Mutex von this verwendet. Die genannten Methoden sind aber allesamt Klassenmethoden, die im Gegensatz zu Instanzmethoden keinen Zugriff auf this haben. Damit stellt sich die Frage: Welches Mutex wird beim Aufruf der synchronized Klassenmethoden gesperrt? Ein objekt-spezifisches Mutex wie das Mutex des this-Objekts kommt offensichtlich nicht in Frage; es wird ein klassenspezifisches Mutex gebraucht. Glücklicherweise gibt es in Java zur Laufzeit für jede Klasse genau ein Objekt vom Typ java.lang.Class, das die Klasse repräsentiert. synchronized Klassenmethoden verwenden das Mutex dieses Class-Objekts. Da das Class-Objekt eindeutig ist (es gibt nur ein einziges Class-Objekt pro Klasse), ist sichergestellt, dass alle Klassenmethoden dasselbe Mutex verwenden und die Zugriffe auf die Klassenvariablen effektiv geschützt sind. Will man den Zugriff auf Instanzvariblen explizit in einem synchronized-Block schützen, so muss man das Klassenobjekt selbst ermitteln. Dies geht zum Beispiel, indem man auf einem Objekt die Methode getClass() aufruft; sie ist in java.lang.Object definiert. Oder man kann es auch direkt über ein Literal im Java Code ansprechen: <vollqualifizierter Packagename>.<Klassename>.class . Damit ist es auch möglich, den Konstruktor von IntStack threadsicher zu implementieren, ohne incInstance() aufzurufen: public IntStack (int sz) {Durch die beschriebene Technik kann man natürlich nicht nur den Zugriff auf einzelne Klassenvariablen vom Typ long oder double schützen. Wenn eine Kombination von Klassenvariablen eine gemeinsam Bedeutung hat (ähnlich wie in unserem Beispiel die Instanzvariablen cnt und array), dann muss der Zugriff auf sie ebenfalls geschützt werden. Dazu kann man dann ebenfalls synchronized Klassenmethoden verwenden. Abweichung vom Standardmuster ?Im allgemeinen wird der Zugriff auf Instanzvariablem mit dem Mutex von this und der Zugriff auf Klassenvariablen mit dem Mutex des Klassenobjekts threadsicher gemacht. Dies geschieht implizit, wenn man synchronized Methoden benutzt. Wenn eine Kombination aus synchronized Blöcken und synchronized Methoden bei der Implementierung einer Klasse verwendet wird, dann muss das jeweils richtige Mutex explizit genannt werden, damit von den Blöcken und den Methoden immer dieselbe Sperre benutzt wird. Wenn man ausschließlich synchronized Blöcke verwendet und keine synchronized Methoden vorkommen, dann hat man die Freiheit, anstelle des Mutex von this oder dem Mutex des Klassenobjekts das Mutex von jedem beliebigen Objekt zu verwenden. Einen solchen Fall wollen wir uns im Folgenden ansehen. Dabei sind folgende Regeln zu beachten:Wenn man die Instanzvariablen einer Instanz schützen will, muss man für alle kritischen Bereiche das gleiche Mutex (und damit das gleiche Objekt verwenden), damit die Sperren effektiv sind. Auch sollte man für jede Instanz der Klasse ein eigenes Mutex verwenden, weil die Instanzen unabhängig voneinander sind. Wenn alle Instanzen dasselbe Mutex verwenden würden, dann würden sich die verschiedenen Instanzen gegenseitig blockieren. Durch die Verwendung eines jeweils anderen Mutex pro Instanz wird die Parallelität des Programms hingegen maximiert, weil sich die Instanzen nicht gegenseitig behindern. Der folgende Code ist ein Beispiel dafür, wie der IntStack mit einem anderen Mutex als dem von this threadsicher gemacht werden kann. Hier wird das Mutex des Objekts myMutex verwendet. public class IntStack {Das Objekt, dessen Mutex für die Synchronisation verwendet wurde, wurde bewusst so gewählt, dass es ansonsten nicht sonderlich nützlich ist: es ist vom Typ int[0]. Häufig sieht man auch, dass das Objekt mit new Object() angelegt wird. An dieser Stelle sollte man sich noch einmal vergegenwärtigen, dass das Mutex am Objekt und nicht an der Variablen hängt. Man verwechselt dies manchmal, weil die Variable in die runden Klammern hinter das synchronized geschrieben wird. Durch die Verwechslung von Variable und Objekt kann es auch zu Fehlern kommen, weil der Variablen ein anderes Objekt zugewiesen werden kann und damit nach der Zuweisung plötzlich ein anderes Mutex verwendet wird. Aus diesem Grund haben wir in unserer Implementierung die Instanzvariable myMutex final gemacht, damit garantiert immer dasselbe Objekt referenziert wird. Welche Unterschiede ergeben sich nun dadurch, dass wir vom Standardmuster abweichen und ein eigenes Mutexobjekt verwenden? Ein Unterschied ergibt sich durch die Sichtbarkeit der Mutexobjekts. Das Aktivieren und Deaktivieren der Sperre kann man auch von außerhalb des Objekts machen, d. h. Sperren und Entsperren entsprechen public Operationen. Da beim Standardmuster das Mutex des Objektes verwendet wird, kann jeder, der Zugriff auf das Objekt hat, die Sperre des Mutex aktivieren. Nehmen wir beispielweise an, in einem Programm steht der folgende Code: . . .Solange dieser Code ausgeführt wird, funktionieren bei einem IntStack entsprechend des Standardmusters die push() und pop() Methoden desjenigen Objekts nicht mehr, das von myIntStack referenziert wird. Das liegt daran, dass die beiden Methoden genau das Mutex sperren wollen, das der oben gezeigte Code für immer in Besitz genommen hat. Mit einem IntStack, der ein eigenes privates Objekt für das Mutex nutzt, kann so etwas nicht passieren. Nun mag es wie ein Fehler erscheinen, wenn das Mutex von Außen zugreifbar ist. Es gibt aber Situationen, in denen die externe Nutzung des Mutex sogar gewollt ist. Um ein Beispiel dafür zu sehen, erweitern wir unseren IntStack (und zwar den mit der Synchronisation nach Standardmuster) um eine Methode iterator(), die einen Iterator vom Typ java.util.Iterator zurückliefert: public Iterator iterator() {Die Implementierung des Iterators ist als anonyme Klasse in der Methode iterator() enthalten. Die remove() Methode ist nicht implementiert, weil wir es nicht zulassen wollen, dass Werte mitten aus dem Stack herausgelöscht werden können. Die next() Methode liefert statt eines int einen Integer zurück, weil int nicht kompatibel zu Object ist. Zur Synchronisation nutzen wir das Mutex der Stack-Instanz, zu der der Iterator gehört. Das heißt, alle Iteratoren einer Stack-Instanz verwenden dasselbe Mutex wie die Instanz selbst. Das ist auch richtig so. Schließlich greifen wir in next() auf cnt und array des Stacks zu. Jetzt kann man in einer beliebigen Klassen die folgende Funktionalität implementieren, die eine Kopie des Inhalt des Stacks in einem int[] zurückgibt: private static int[] stackToArray(IntStack is) {Hier ist es enorm wichtig, dass wir einen synchronized Block mit dem Mutex des Stacks nutzen können. Dadurch ist sichergestellt, dass sich der Stack über die gesamte Iteration nicht ändert. So können wir zum Beispiel sicher sein, dass die Länge des Arrays, die wir am Anfang ermitteln, während der Ausführung des gesamten kritischen Bereichs tatsächlich der Größe des Stacks entspricht und nicht etwa mittendrin von einem anderen Thread geändert wird. Genauso wichtig ist es, dass sich der Stack zwischen dem Aufruf von hasNext() und next() nicht verändern kann. Mit einem IntStack, der nicht nach dem Standardmuster sondern mit einem privaten Objekt für das Mutex implementiert ist, können wir so etwas nicht erreichen. Fassen wir unsere Erfahrungen mit dem Standardmuster bzw. einer möglichen Abweichung davon (mit privatem Mutex Objekt) noch einmal zusammen: Immer wenn man dem Nutzer die Möglichkeit geben will (oder sogar muss), mehrere Operationen durch das Mutex des Objekts zu schützen, so wird man das Standardmuster nutzen. Ist dies nicht nötig und möchte man das Mutex nicht für andere zugreifbar halten, wird man ein explizites privates Mutex nutzen.
Die Diskussion, die wir hier für Instanzvariablen und –methoden
geführt haben, gilt natürlich in abgewandelter Form auch für
Klassenmethoden.
ZusammenfassungWir haben uns in dieser Ausgabe die Verwendung des Schlüsselwortes synchronized genauer angesehen. Dabei haben wir gesehen, dass synchronized-Blöcke und -Methoden implizit ein Mutex verwenden. Mutexe sind in Java nicht sichtbar und auch nicht explizit zugreifbar, aber jedes Objekt hat ein solches Mutex. synchronized Instanzmethoden verwenden automatisch das Mutex von this; synchronized Klassenmethoden verwenden automatisch das Mutex des Class-Objekt, welches die Klasse repräsentiert. Bei synchronized–Blöcken kann man davon abweichend ein ganz anderes Mutex verwenden.Literaturverweise
|
|||||||||||||||||||
© Copyright 1995-2015 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/13.synchronized/13.synchronized.html> last update: 17 Jun 2015 |