|
|||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||
|
Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ
|
||||||||||||||||||||||||
Wir haben in einem der vorangegangenen Beiträge [
EFF2
]
erwähnt, dass es besondere Garantieren für die Sichtbarkeits-
und Speichereffekte im Zusammenhang mit final gibt. Diese Garantien
wollen wir uns in diesem Beitrag genauer ansehen.
Racy-Single-Check mit einem primitiven Typ (außer long und double)Als wir im letzten Artikel [ EFF5 ] die Verwendung von volatile am Beispiel des Double-Check-Idioms besprochen haben, haben wir uns auch das sogenannte Racy-Single-Check-Idiom angesehen. Bei diesem Idiom wird weder Synchronisation noch volatile genutzt. Entsprehend ist seine Anwendung sehr eingeschränkt, aber es gibt Fälle, in denen es korrekt und unproblematisch ist, nämlich wenn das zu initialisierende Feld einen kostanten Wert zugewiesen bekommt und von einem primitiven Typ (außer long und double).Hier noch einmal unser Beispiel des Racy-Single-Check -Idioms aus unserem letzten Artikel: public class MyClass {Für die korrekte Verwendung des Racy-Single-Check-Idiom im Beispiel oben sind folgende Überlegungen wichtig:
Racy-Single-Check mit einem ReferenztypWie funktioniert nun das Racy-Single-Check-Idiom mit einem Feld, dass von einem Referenztyp ist?Hier ist ein Beispiel mit einer Referenz auf einen Integer vom Typ java.lang.Integer : public class MyClass {Hier gehen nun drei Überlegungen ein, die die korrekte Verwendung des Racy-Single-Check-Idiom garantieren:
Anforderungen an unveränderliche TypenNun ist der Typ java.lang.Integer bekanntlich ein unveränderlicher Typ und er ist auch so implementiert, dass das Racy-Single-Check-Idiom mit einem Integer funktioniert, aber das gilt nicht für jeden Typ, der von sich behauptet unveränderlich zu sein. Es genügt nämlich 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 sicher stellen, dass die unveränderlichen Inhalte des Objekts nach der Konstruktion allen benutzenden Threads sichtbar werden. Denn was nützt es uns, wenn die Threads zwar die Adresse des Integers nach der Lazy-Initialisierung sehen, aber nicht die Inhalte des Integers? 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. Das ist eine Regel für die Sichtbarkeit der Inhalte der final-Felder von Objekten.
Sehen wir uns also die "Initialization-Safety"-Garantie des Java Memory
Modells mal genauer an.
Speichereffekte im Zusammenhang mit final-FeldernEs geht bei der "Initialization Safety"-Garantie des Java Memory Modells 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.Was heißt das genau? Betrachten wir ein erstes einfaches Beispiel einer Klasse mit einem final-Feld: public class Immutable {Die Klasse ist dem unveränderlichen Typ java.lang.Integer nachempfunden. Sie könnte beispielsweise als Typ des lazyField in unserem Racy-Single-Check-Idiom vorkommen. public class MyClass {Nehmen wir nun einmal 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. Wenn das Feld field in der Klasse Immutable nicht final wäre, dann könnte es in dieser Situation passieren, dass der zweite Thread das referenzierte Immutable -Objekt sieht und das Feld field in diesem Objekt entweder den Wert 0 oder 10000 hat. Welchen der beiden Werte das Feld hat, ist undefiniert. Es kann sogar passieren, dass der zweite Thread, wenn er mehrmals liest, erst den Wert 0 und später den Wert 10000 zu sehen bekommt. Das sieht dann so aus, als sei das unveränderliche Objekt vom Typ Immutable gar nicht unveränderlich, weil sich sein Inhalt augenscheinlich ändert. Wie kann das sein? Solche Effekte können bei fehlender final-Deklaration entstehen, weil es ohne die final-Deklaration keinerlei Garantien für die Sichtbarkeit des Feldes field gibt. Sichtbarkeitsprobleme im DetailSichtbarkeitsprobleme sind generell ein bisschen schwierig zu verstehen, weil sie immer Probleme zwischen Threads sind und nie innerhalb eines Threads entstehen. Ein Sichtbarkeitsproblem gibt es nur dann, wenn ein Thread beobachtet, was ein anderer Thread im Speicher macht. In unserem Beispiel ist es so, dass der erste Thread eine null -Referenz vorfindet und dann die lazy-Initialisierung macht, also das Immutable -Objekt konstruiert und dessen Adresse in der Referenzvariablen lazyField ablegt. So sieht es innerhalb des ersten Threads aus und es entspricht unserer Intuition: erst wird das Objekt alloziert (dann ist es in einem Defaultzustand), dann konstruiert (dann ist es in seinem Initialzustand) und danach wird seine Adresse dem Feld lazyField zugewiesen.Für den zweiten Thread sieht die Sache u.U. fundamental anders aus. In der oben geschilderten Situation haben wir angenommen, dass das int-Feld in der Klasse Immutable nicht final ist und es daher keine Sichtbarkeitsgarantien gibt. Dann ist undefiniert, welche von den Speichermodifikationen, die der erste Thread gemacht hat, überhaupt sichtbar wird. Wir haben mal angenommen, dass ein Teil sichtbar wird, nämlich die Adresse des konstruierten Immutable -Objekts, aber nicht dessen Inhalt. Das ist ein denkbares Szenario; es kann auch passieren, dass der zweite Thread gar nichts zu sehen bekommen, auch nicht die Adresse des neu erzeugten Objekts. Aber nehmen wir mal an, die Adresse wird sichtbar und der Rest nicht. Dann sieht der zweite Thread zwar das neu erzeugte Immutable -Objekt, aber er sieht den Inhalt des Objekts in seinem Defaultzustand vor der Initialisierung der Felder, d.h. er sieht für das int-Feld den Wert 0. Der erste Thread hat zwar das Immutable -Objekt ordnungsgemäß initialisiert, ehe dessen Adresse in der Referenzvariablen lazyField abgelegt wurde, und in dem int-Feld des Immutable -Objekts steht auch der beabsichtigte Initialwert 10000 drin, aber dieser Wert 10000 existiert nur im Cache des ersten Threads und nicht im Hauptspeicher (oder er steht im Hauptspeicher und der zweite Thread hat seinen Cache nicht aufgefrischt). Wie auch immer die Konstellation genau sein mag, der Effekt ist, dass es für den zweiten Thread so aussieht, als sähe er das Objekt vor seiner Konstruktion. Im weiteren zeitlichen Verlauf kann es dann noch passieren, dass der zweite Thread vielleicht noch einmal auf des Objekt schaut und in der Zwischenzeit Flushes und Refreshes erfolgt sind, so dass der Initialwert 10000 mittlerweile sichtbar geworden ist. Dann sieht es für den zweiten Thread so aus, als habe sich das Immutable -Objekt von seinem Defaultzustand in seinen Initialzustand geändert. Wie gesagt, es sieht nur so aus. Dahinter stehen die mehr oder weniger zufällig passierenden Speichereffekte. Weil "mehr oder weniger zufällig passierende Speichereffekte" keine verlässlich funktionierenden Programme ergeben, will man genau solche undefinierten Situationen nicht haben. Die beschriebenen Effekte sind in der Regel höchst unerwünscht und deshalb haben wir das int-Feld ganz bewußt als final deklariert. Dann profitieren wir nämlich von der "Initialization Safety"-Garantie des Java Memory Modells. Es garantiert, dass das final-Feld erst nach seiner Initialisierung sichtbar gemacht wird und niemals vorher. Das heißt, wenn ein anderer Thread die Adresse des neu konstruierten Objekts zu sehen bekommt, dann sind garantiert alle final-Felder des neuen Objekts bereits in ihrer initialisierten Form sichtbar. Für die Implementierung eines unveränderlichen Typs bedeutet es, dass grundsätzlich alle seine Felder als final deklariert sein müssen. Unser Immutable -Typ im Beispiel ist korrekt implementiert: er hat keine verändernden Methoden und alle seine Felder sind als final deklariert. Man kann ihn ohne Bedenken als Typ eines Felds verwenden, das mit dem Racy-Single-Check-Idiom initialisiert wird. Das gilt aber, wie gesagt, nicht für alle Typen, die von sich behaupten, unveränderlich zu sein. Wenn ein angeblich unveränderlicher Typ non-final-Felder hat, dann ist Vorsicht geboten, weil die oben ausführlich beschriebenen Sichtbarkeitsprobleme auftreten können. Mögliche MissverständnisseAn dieser Stelle ist vielleicht noch ein Hinweis auf mögliche Mißverständnisse angebracht: Es ist zu beachten, dass die "Initialization Safety"-Garantie nur für die final-Felder eines Objekts gilt. Wenn das Objekt noch weitere Felder hat, die nicht als final deklariert sind, dann gibt es für diese non-final-Felder keine Garantien. Hier ein Beispiel zur Illustration:public class NoLongerImmutable {Die Klasse NoLongerImmutable hat ein final- und ein non-final-Feld. Selbst wenn die Klasse nur lesende Methoden hat, ist sie ist unveränderlicher Typ nicht mehr wirklich brauchbar, zumindest nicht im Zusammenhang mit dem Racy-Single-Check-Idiom. Wenn wir sie so verwenden wie zuvor die Klasse Immutable , dann gibt es Sichtbarkeitsprobleme für das non-final-Feld. public class MyClass {Hier könnte der zweite Thread [10000/10000] ausgeben, oder aber auch [10000/0], weil das zweite Feld nicht final ist und deshalb unklar ist, ob sein Default- oder sein Initialwert sichtbar wird. ZusammenfassungIn diesem Beitrag haben wir uns die "Initialization Safety"-Garantie für final-Felder angesehen. Die Garantie besagt, dass final-Felder eines Objekts einem andern Thread stets in ihrer fertig initialisierten Form sichtbar werden, niemals vorher. Für non-final-Felder gibt es keine Garantien.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, sonst kann es Sichtbarkeitsprobleme geben, beispielsweise wenn der unveränderliche Typ für eine lazy-Initialisierung mit dem Racy-Single-Check-Idiom verwendet wird.
Wir haben die gesamte Diskussion auf final-Felder von primitivem Typ
beschränkt. Wie ist das, wenn das final-Feld von einem Referenztyp
ist? Das sehen wir uns beim nächsten Mal 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/43.JMM-InitializationSafety.1/43.JMM-InitializationSafety.1.html> last update: 22 Mar 2015 |