|
|||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||
|
Die Initialisation-Safety-Garantie für final-Felder von einem Referenztyp
|
||||||||||||||||||||||||||
Wir haben im letzten Beitrag [ JMM6 ] die Initialisation-Safety-Garantien des Java Memory Modells für final-Felder besprochen. Dabei geht es um die Garantie, dass alle final-Felder eines Objekts stets in ihrem Zustand nach der Konstruktion und nie in ihrem Defaultzustand vor der Konstruktion sichtbar werden. Wir haben dabei ein final-Feld vom Typ int, also einem primitiven Typ, betrachtet. Wie sehen die Garantien für final-Felder von einem Referenztyp aus? Werden auch die referenzierten Objekte sichtbar oder nur die Referenz selbst? Das wollen wir uns in diesem Beitrag ansehen. Ausgangspunkt unserer Diskussion war die Lazy-Initialisierung eines Feldes mit Hilfe des Racy-Single-Check-Idioms (siehe [ JMM5 ]). Bei diesem Idiom wird weder Synchronisation noch volatile genutzt. Hier ist ein Beispiel mit einer Referenz auf einen Integer vom Typ java.lang.Integer: public class MyClass {Für die korrekte Verwendung des Racy-Single-Check-Idiom müssen folgende Voraussetzungen gegeben sein:
Anforderungen an unveränderliche TypenEs genügt nicht, dass es in einem unveränderlichen Typ keine modifizierenden Methoden gibt. Ein unveränderlicher Typ muss auch für die Sichtbarkeit seiner Inhalte sorgen, das heißt, er muss sicherstellen, dass die unveränderlichen Inhalte des Objekts nach der Konstruktion allen benutzenden Threads sichtbar werden. Um für die Sichtbarkeit zu sorgen, braucht man bei der Implementierung eines unveränderlichen Typs die sogenannte "Initialization-Safety"-Garantie des Java Memory Modells.Bei der "Initialization Safety"-Garantie des Java Memory Modells geht es darum, dass stets die Initialwerte von final-Feldern und niemals die Defaultwerte sichtbar sind. Wenn also ein Thread ein Objekt mit final-Feldern zu sehen bekommt, weil er die Adresse des Objekts sehen kann, dann sieht er die final-Felder des Objekts stets im Initialzustand nach der Konstruktion und nie im Defaultzustand vor der Konstruktion. Wir haben dazu ein Beispiel mit einer Klasse mit einem final-Feld betrachtet: public class Immutable {Der unveränderlichen Typ Immutable wird für ein Feld verwendet, das mit dem Racy-Single-Check-Idiom "lazy" initialisiert wird: public class MyClass {Dann nehmen wir an, dass zwei Threads gleichzeitig auf ein Objekt vom Typ Immutable zugreifen: public class Test {Beide Threads holen sich über die Methode getMyField() der Klasse MyClass die Referenz auf das Immutable-Feld des MyClass-Objekts und rufen anschließend auf dem Immutable-Feld die toString()-Methode der Klasse Immutable auf. Dann könnte es so auskommen, dass der eine Thread in der Methode getMyField() die Referenz lazyField auf das Immutable-Feld noch als null vorfindet, weil noch niemand die lazy-Initialisierung für das Feld gemacht hat. Der andere Thread findet möglicherweise schon eine von null verschiedene Referenz vor und greift über diese Referenz auf das Immutable-Objekt zu und ruft dessen toString()-Methode auf. In dieser Situation stellt sich die Frage, in welchem Zustand der zweite Thread den Inhalt des referenzierten Immutable-Objekts zu sehen bekommt.
Da das int-Feld field in der Klasse Immutable als final deklariert ist,
garantiert die Initialisation-Safety-Garantie, dass der zweite Thread das
Immutable-Objekt in seinem fertig initialisierten Zustand zu sehen bekommt.
Der Wert des int-Feld field ist 10000 und nicht etwa 0, wie es bei fehlender
final-Deklaration der Fall sein könnte.
Sichtbarkeitsgarantieren für abhängige ObjekteWie ist das nun, wenn das final-Feld in einem unveränderlichen Typ kein Wert von einem primitiven Typ wie int ist, sondern eine Referenz auf ein Objekt? Hier ein Beispiel, in dem die Klasse Immutable kein final-Feld vom Typ int, sondern eine final-Referenz auf ein Array enthält:public class Immutable {Zunächst einmal ist klar, dass der zweite Thread die Adresse des Arrays sehen wird (und nicht etwa null), weil die Referenzvariable finalArrayRef als final deklariert ist. Es stellt sich aber die Frage, ob der lesende Thread auch die Elemente in dem Array zu sehen bekommt. Glücklicherweise gibt das Java Memory Modell in der Tat derartige Garantien für die Array-Elemente. Die Garantie für final-Felder bezieht sich nämlich nicht nur auf die final-Felder selbst. Für final-Felder, die von einem Referenztyp sind, ist garantiert, dass die Referenz und alle "abhängigen" Objekte sichtbar gemacht werden. Die "abhängigen" Objekte sind jene, die von einem final-Feld aus per Referenz erreichbar sind, und alle Objekte, die wiederum von dort aus per Referenz erreichbar sind. Gemeint ist also die gesamte transitive Hülle aller erreichbaren Objekte. Das würde in unserem Beispiel alle Array-Elemente einschließen. In dem SMP-Modell für das Java Memory, das wir in [ JMM2 ] beschrieben haben, kann man es sich so vorstellen, als ob am Ende der Konstruktion eines Objekts mit final-Feldern ein partieller Flush ausgelöst würde, bei dem die final-Felder des Objekts und alle "abhängigen" Objekte in den Hauptspeicher zurückgeschrieben werden. In jedem Falle ist garaniert, dass alle final-Felder eines Objekts und alle von diesen final-Feldern aus erreichbaren Objekte anderen Thread sichtbar gemacht sind, ehe die Adresse des Objekts sichtbar wird und die anderen Threads auf die final-Felder zugreifen können. Unser Immutable-Typ mit dem Array wäre also korrekt implementiert: er hat keine verändernden Methoden, alle seine Felder sind als final deklariert und die Initialisation-Safety-Garantie sorgt dafür, dass auch die Array-Elemente sichtbar werden. Man kann ihn ohne Bedenken als Typ eines Felds verwenden, das mit dem Racy-Single-Check-Idiom initialisiert wird. Wie ist das nun, wenn der unveränderliche Typ eine Referenz auf ein Array mit Referenzen (statt primitiven Elementen) enthält, also kein int[], sondern ein Integer[]? Die Initialisation-Safety-Garantie sorgt dann dafür, dass auch die von den Array-Elementen referenzierten Objekte und deren Inhalte sichtbar werden. Damit der äußere Typ Immutable unververänderlich ist, müssen die Array-Elemente natürlich wiederum von einem korrekt implementierten unveränderlichen Typ (wie z.B. Integer oder String) sein.
Was wir hier am Beispiel einer final-Referenz auf ein Array erläutert
haben, gilt analog für final-Referenzen auf Objekte. Die Initialisation-Safety-Garantie
sorgt dafür, dass die gesamte transitive Hülle aller erreichbaren
Objekte am Ende der Konstruktion sichtbar wird.
Mögliche MißverständnisseMit der Initialisation-Safety-Garantie muss man übrigens manchmal etwas vorsichtig umgehen. Im lesenden Thread ist nur gewährleistet, dass er sich beim ersten Zugriff auf das Objekt vom Typ Immutable alle Werte der final-Felder aus dem Hauptspeicher holt. Dabei holt er sich auch die Werte aller abhängigen Objekte; er macht also einen partiellen Refresh seines Arbeitsspeichers aus dem Hauptspeicher. Danach muss er aber keinen Refresh mehr machen. Das ist auch sinnvoll so. Für die final-Referenzvariable braucht er sowieso keinen Refresh mehr, weil die Adresse des Arrays konstant ist und sich nicht mehr ändern kann. In einem unveränderlichen Typ ändern sich auch die Inhalte des Arrays und seiner Elemente nicht; sonst wäre der Typ veränderlich.Die Unveränderbarkeit des Arrays ist aber durch nichts gewährleistet, weil die final-Deklaration der Array-Referenz nur sagt, dass die Adresse des Arrays konstant ist, nicht aber der Inhalt des Array selbst. Es wäre also prinzipiell möglich, dass sich das Array und seine Elemente ändern. Für die Sichtbarkeit dieser späteren Änderungen gibt das Java Memory Modell keine Garantien. Sehen wir uns das im Beispiel mal an: public class NoLongerImmutable {Hier ist nun nicht garantiert, dass lesende Threads die Modifikation an den Array-Elementen zu sehen bekommt, die andere Threads mit Hilfe von update() nach der Konstruktion gemacht haben. Lesende Threads müssen nur ein einziges Mal einen Refresh ihres Arbeitsspeichers machen, nämlich beim ersten Zugriff auf das final-Feld und alle seine abhängigen Objekte. Veränderungen an den abhängigen Objekten, die später noch passieren, müssen nicht - aber könnten (zum Beispiel durch weitere Synchronisationspunkte an ganz anderen Stellen) - sichtbar werden. Wenn sie garantiert sichtbar werden sollen, dann muss man mit anderen Mitteln (zum Beispiel explizite Synchronisation) für die Sichtbarkeit sorgen. Man beachte, dass dieses Missverständnis bei unveränderlichen Typen nicht auftreten kann, weil in ein einem unveränderlichen Typ alle abhängigen Objekte ebenfalls unveränderlich sind. Es genügt also der Refresh aller abhängigen Objekte beim allerersten Zugriff, weil sich danach nichts mehr an den abhängigen Objekten ändert. Unterschiede zu volatileMan beachte, dass die Speichereffekte bei final-Feldern ganz anders sind als bei volatile-Referenzvariablen. Bei volatile-Variablen löst jeder schreibende oder lesende Zugriff einen Flush bzw. Refresh des gesamten Arbeitsspeichers aus (siehe [ JMM4 ]). Bei final-Referenzvariablen löst nur das Ende des Konstruktors einen partiellen Flush und nur der erstmalige lesende Zugriff in jedem Thread einen partiellen Refresh aus. volatile ist also "teuer" als final, weil mehr Memory Barriers ausgelöst werden, und volatile hat keine Garantie für abhängige Objekte.final Variablen vs. final FelderNoch ein Hinweis auf mögliche Missverständnisse: "final" ist nicht gleich "final". Die Garantien des Java Memory Modells im Zusammenhang mit final beziehen sich nur auf final-Felder von Objekten, nicht auf final-Variablen. Die Speichereffekte für final werden nicht ohne Grund als "Initialization Safety"-Garantie bezeichnet. Die Garantie besagt lediglich, dass final-Felder eines Objekts stets in ihrer fertig initialisierten Form sichtbar werden, niemals vorher. Für andere Verwendungen von final (zum Beispiel als Parameter und lokalen Variablen von Methoden) gibt es keine Garantien.Schauen wir uns zur Illustration einen Fall an, in dem zwar final verwendet wird, seine Verwendung aber nichts mit Initialization Safety zu tun hat. public class ActiveObject{Das Beispiel zeigt ein gängiges Idiom für die Parametrisierung von Runnables: da die run()-Methode keine Argumente haben darf, implementiert man Runnables gerne als anonyme innere Klassen und gibt ihnen Zugriff auf final-Variablen des umgebenden Kontextes. Auf diese Art erreicht man eine indirekte Parametrisierung der run()-Methode. In diesem Beispiel haben der main-Thread und der printer-Thread gemeinsam und konkurrierend Zugriff auf das final-Array argumentAndResult. Das erste Array-Element ist der Parameter für den printer-Thread; in dem zweiten Array-Element legt der printer-Thread das Ergebnis ab, d.h. die Zeit, die für das Ausdrucken von times vielen "*" gebraucht wurde. Hier hat die Verwendung von final überhaupt nichts mit den oben besprochenen Speichereffekten für final-Felder zu tun, denn hier brauchen wir gar keine Garantien im Zusammenhang mit final-Variablen. Die benötigten Speichererffekte werden hier durch Thread-Start und Thread-Ende geliefert, nicht durch die final-Deklaration der Array-Referenz. Der Start des printer-Threads löst einen Flush im main-Thread und einen Refresh im printer-Thread aus. Das heißt, der neu gestartete printer-Thread kann sehen, was der ihn startende main-Thread gemacht hat. Analog beim Thread-Ende: der main-Thread wartet mit Thread.join() auf das Ende des printer-Threads und kann dann alles sehen, was der printer-Thread gemacht hat.
Und eine letzter Hinweis auf mögliche Mißverständnisse:
ZusammenfassungIn diesem Beitrag haben wir uns die "Initialization Safety"-Garantie für final-Felder von einem Referenztyp angesehen. Die Garantie besagt, dass final-Felder eines Objekts einem andern Thread stets in ihrer fertig initialisierten Form sichtbar werden, niemals vorher. Dabei werden auch alle abhängigen Objekte in ihrer fertig initialisierten Form sichtbar.Diese Garantie gibt es nur für final-Felder und nicht für final-Variablen. Die "Initialization Safety"-Garantie wird für die Implementierung von unveränderlichen Typen gebraucht: in einem unveränderlichen Typ müssen alle Felder als final deklariert sein und alle abhängigen Objekte müssen ihrerseits unveränderlich sein.
Im nächsten Beitrag sehen wir uns, was man trotz Verwendung von
final bei der Implementierung von unveränderlichen Typen falsch machen
kann. Die "Initialization Safety"-Garantie gilt nämlich nur, wenn
der sogenannte Object Escape verhindert wird. Das schauen wir uns im nächsten
Beitrag genauer an.
LiteraturverweiseDie gesamte Serie über das Java Memory Model:
|
|||||||||||||||||||||||||||
© Copyright 1995-2015 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/44.JMM-InitializationSafety.2/44.JMM-InitializationSafety.2.html> last update: 22 Mar 2015 |