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 einem Referenztyp

Die Initialisation-Safety-Garantie für final-Felder von einem Referenztyp
Java Memory Model
Die Initialisation-Safety-Garantie für final-Felder von einem Referenztyp

Java Magazin, April 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 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 {
    private Integer lazyField = null;  // default value
    ...

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

        return lazyField;
    }

    ...
}

Für die korrekte Verwendung des Racy-Single-Check-Idiom  müssen folgende Voraussetzungen gegeben sein:
  • Der Zugriff auf das fragliche Feld muss atomar sein. Das ist hier der Fall, weil das Feld von einem Referenztyp ist.
  • Die Referenz lazyField ändert sich nach der Initialisierung nicht mehr.  Um das sicherzustellen, würde man das Feld gerne als final deklarieren, aber es geht 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. Deshalb 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 betreffende Feld eine unveränderliche Referenz auf einen unveränderlichen (immutable) Typ ist.  Deshalb haben wir über unveränderliche Typen gesprochen und festgestellt, dass alle Felder eines unveränderlichen Typs als final deklariert sein müssen; sonst gibt es Sichtbarkeitsprobleme.  Die Sichtbarkeitsprobleme haben wir beim letzten Mal ausführlich diskutiert (siehe [ JMM5 ]).
 

Anforderungen an unveränderliche Typen

Es 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 {
  private final int field;
  public Immutable (int init) {
    field = init;
  }
  public String toString() {
    return "["+ field +"]";
  }
}
Der unveränderlichen Typ Immutable wird für ein Feld verwendet, das mit dem Racy-Single-Check-Idiom "lazy" initialisiert wird:
public class MyClass {
    private Immutable lazyField = null;  // default value
    ...

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

        return lazyField;
    }
    ...
}

Dann nehmen wir 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.

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 Objekte

Wie 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 {
  private final int[] finalArrayRef;
  public Immutable(int start, int size) {
    finalArrayRef = new int[size];
    for (int i=0;i<size;i++)
       finalArrayRef[i] = start+i;
  }
  public String toString() {
    return "["+ Arrays.toString(finalArrayRef) +"]";
  }
}
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ändnisse

Mit 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 {
  private final int[] finalArrayRef;
  public NoLongerImmutable(int start, int size) {
    finalArrayRef = new int[size];
    for (int i=0;i<size;i++)
       finalArrayRef[i] = start+i;
  }
  public String toString() {
    return "["+ Arrays.toString(finalArrayRef) +"]";
  }
  public void update(int idx, int val) {
    finalArrayRef[idx] = val;
  }
}
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 volatile

Man 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 Felder

Noch 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{
  public ActiveObject(long times) throws InterruptedException {
    final long[] argumentAndResult = new long[2];
    argumentAndResult[0] = times;

    Runnable r = new Runnable() {
      public void run() {
        long start = System.nanoTime();
        for (int i=0;i<argumentAndResult[0];i++)
           System.out.print("*");
        argumentAndResult[1] = System.nanoTime()-start;
      }
    };
    Thread t = new Thread(r,"printer");
    t.start();
    t.join();
    System.out.println(argumentAndResult[1]);
  }
}

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:
Für die Thread-Ende-Garantie ist übrigens wichtig, dass sich der eine Thread über das Ende des anderen Threads aktiv informiert hat (per join() oder isAlive()).  Wenn der printer-Thread nur zufällig schon gerade fertig ist, dann ist nicht garantiert, dass der main-Thread sieht, was der printer-Thread gemacht hat, weil der Refresh im main-Thread erst durch join() oder isAlive() ausgelöst wird.

Zusammenfassung

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

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
/JMM9/ Über die Gefahren allzu aggressiver Optimierungen
Klaus Kreft & Angelika Langer, Java Magazin, Juni 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/45.JMM-AggressiveOpt/45.JMM-AggressiveOpt.html

 
 

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/44.JMM-InitializationSafety.2/44.JMM-InitializationSafety.2.html  last update: 22 Mar 2015