Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ

Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ
Java Memory Model
Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ
 

Java Magazin, Februar 2009
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).
 
Die ganze Serie zum  "Java Memory Modell" als PDF (985 KB).
 

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 {
    private int lazyField;
    ...

    public int getMyField() {
        if (lazyField == 0)
            lazyField = 10000;

        return lazyField;
    }

    ...
}

Für die korrekte Verwendung des Racy-Single-Check-Idiom im Beispiel oben sind folgende Überlegungen wichtig:
  • Der Zugriff auf das fragliche Feld muss atomar sein. Sonst wäre der Zugriff unterbrechbar und das Feld könnte (aus Sicht des lesenden Threads) sinnlose Werte enthalten. Die Ununterbrechbarkeit des Zugriffs auf primitive Typen (außer long und double) ist in Java garantiert.
  • Das Feld lazyField ändert sich nach der Initialisierung nicht mehr.   Man würde es gerne als final deklarieren, um diese Eigenschaft sichtbar zu machen, aber das geht nicht, weil die Initialisierung nicht im Konstruktor, sondern "lazy" in der Methode getMyField() erfolgt. Semantisch ist das Feld aber final, sonst wäre die Verwendung des Racy-Single-Check-Idiom falsch.  Es könnte sonst passieren, das zwei Thread gleichzeitig " lazyField == 0 " sehen und beide nacheinander den Initialwert zuweisen. Wenn zwischendrin ein dritter Thread das Feld bereits verändert hätte, dann ginge diese Veränderung verloren.  Es ist also wichtig, dass der Inhalt des Feldes unveränderlich ist.
Ein bekanntes Anwendungsbeispiel für das Racy-Single-Check-Idiom ist der Hashcodewert in der Klasse java.lang.String . Er wird genau mit dieser Technik lazy initialisiert.
 

Racy-Single-Check mit einem Referenztyp

Wie 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 {
    private Integer lazyField = null;  // default value
    ...

    public Integer getMyField() {
        if (lazyField == null)
            lazyField = new Integer(10000);

        return lazyField;
    }

    ...
}

Hier gehen nun drei Überlegungen ein, die die korrekte Verwendung des Racy-Single-Check-Idiom garantieren:
  • Der Zugriff auf das fragliche Feld muss atomar sein . Das ist auch hier der Fall; die Ununterbrechbarkeit des Zugriffs auf Adressen, also Variablen von einem Referenztyp, ist in Java garantiert.
  • Die Referenz lazyField ändert sich nach der Initialisierung nicht mehr.   Auch hier würde man das Feld gerne als final deklarieren, aber es geht wieder nicht wegen der "lazy" Initialisierung. Die final-Deklaration der Referenz bezöge sich ohnehin nur auf die Adresse des referenzierten Objekts und nicht auf das Objekt und seine Inhalte.  Ein unveränderliche Adresse reicht aber für das Racy-Single-Check-Idiom nicht aus; es muss eine dritte Voraussetzungen erfüllt sein.
  • Das referenzierte Objekt und seine Inhalte ändern sich nach der Initialisierung nicht mehr. Wenn das Objekt veränderlich wäre, dann wäre die Verwendung des Racy-Single-Check-Idiom falsch, denn das Idiom lässt Mehrfach-Initialisierung zu.  Es könnte passieren, das zwei Thread gleichzeitig " lazyField == null " sehen und beide nacheinander den Initialwert zuweisen. Eine Veränderung des referenzierten Objekts, die zwischendrin ein dritter Thread vorgenommen hätte, ginge verloren.  Es ist also wichtig, dass nicht nur die Referenz, sondern auch das referenzierte Objekt unveränderlich ist.
Das bedeutet, dass das Racy-Single-Check-Idiom nur sinnvoll ist, wenn das Feld eine unveränderliche Referenz auf einen unveränderlichen (immutable) Typ ist.
 

Anforderungen an unveränderliche Typen

Nun 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-Feldern

Es 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 {
  private final int field;
  public Immutable (int init) {
    field = init;
  }
  public String toString() {
    return "["+ field +"]";
  }
}
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 {
    private Immutable lazyField = null;  // default value
    ...

    public Immutable getMyField() {
        if (lazyField == null)
            lazyField = new Immutable(10000);

        return lazyField;
    }
    ...
}

Nehmen wir nun einmal an, dass zwei Threads gleichzeitig auf ein Objekt vom Typ Immutable zugreifen.
public class Test {
  private static MyClass globalRef = new MyClass();

  public static void main(String[] args) {
    Runnable r = new Runnable() {
                 public void run() {
                   System.out.println(globalRef.getMyField().toString());
                 }
               };
    new Thread(r).start();
    new Thread(r).start();
  }
}

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 Detail

Sichtbarkeitsprobleme 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ändnisse

An 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 {
  private final int finalField;
  private       int nonFinalField;
  public NoLongerImmutable(int init) {
    finalField = init;
    nonFinalField = init;
  }
  public String toString() {
    return "["+ finalField + "/" + nonFinalField +"]";
  }
  …
}
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 {
    private NoLongerImmutable lazyField = null;  // default value
    ...

    public NoLongerImmutable getMyField() {
        if (lazyField == null)
            lazyField = new NoLongerImmutable(10000);

        return lazyField;
    }
    ...
}

public class Test {
  private static MyClass globalRef = new MyClass();

  public static void main(String[] args) {
    Runnable r = new Runnable() {
                 public void run() {
                   System.out.println(globalRef.getMyField().toString());
                 }
               };
    new Thread(r).start();
    new Thread(r).start();
  }
}

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.

Zusammenfassung

In 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.
 

Literaturverweise

Die gesamte Serie über das Java Memory Model:

/JMM1/ Einführung in das Java Memory Model: Wozu braucht man volatile?
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/37.JMM-Introduction/37.JMM-Introduction.html
/JMM2/ Überblick über das Java Memory Model
Klaus Kreft & Angelika Langer, Java Magazin, August 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/38.JMM-Overview/38.JMM-Overview.html
/JMM3/ Die Kosten der Synchronisation
Klaus Kreft & Angelika Langer, Java Magazin, September 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/39.JMM-CostOfSynchronization/39.JMM-CostOfSynchronization.html
/JMM4/ Details zu volatile-Variablen
Klaus Kreft & Angelika Langer, Java Magazin, Oktober 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/40.JMM-volatileDetails/40.JMM-volatileDetails.html
/JMM5/ volatile und das Double-Check-Idiom
Klaus Kreft & Angelika Langer, Java Magazin, November 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/41.JMM-DoubleCheck/41.JMM-DoubleCheck.html
/JMM6/ Regeln für die Verwendung von volatile
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/42.JMM-volatileIdioms/42.JMM-volatileIdioms.html
/JMM7/ Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/43.JMM-InitializationSafety.1/43.JMM-InitializationSafety.1.html
/JMM8/ Die Initialisation-Safety-Garantie für final-Felder von einem Referenztyp
Klaus Kreft & Angelika Langer, Java Magazin, April 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/44.JMM-InitializationSafety.2/44.JMM-InitializationSafety.2.htm l

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Concurrent Java - An in-depth seminar covering all that is worth knowing about concurrent programming in Java, from basics such as synchronization over the Java 5.0 concurrency utilities to the intricacies of the Java Memory Model (JMM).
4 day seminar ( open enrollment and on-site)
 

 
  © 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