Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | 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 
NEWSLETTER 
CONTACT 
volatile und das Double-Check-Idiom

volatile und das Double-Check-Idiom
Java Memory Model
volatile und das Double-Check-Idiom

Java Magazin, November 2008
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 den letzten Beiträgen diskutiert, dass Synchronisation mit Hilfe von Locks die Skalierbarkeit bei Mulit-Core- und Multiprozessor-Architekturen einschränkt (/ JMM3 /). Um dies zu vermeiden, kann die Daten-Synchronisation durch die Verwendung von volatile-Variablen erreicht werden.  Dazu haben wir uns im letzten Beitrag im Detail angesehen, welche Speichereffekte der lesende bzw. schreibende Zugriff auf volatile Variablen hat(/ JMM4 /). Die dort verwendeten Beispiele hatten aber eher didaktischen Charakter. Was uns also bis heute noch fehlt, ist ein überzeugendes Beispiel aus der Praxis, das zeigt, wie die Verwendung von volatile die Skalierbarkeit verbessert. Das liefern wir in diesem Artikel nach.

Ausgangssituation

Wir hatten in unserem letzen Beitrag schon erwähnt, dass volatile und Synchronisation mit Locks keine völlig gleichwertigen Konzepte sind (/ JMM4 /). Deshalb gibt es auch kein einfaches ‚Kochrezept’, das man anwenden kann, um Locks durch volatile-Variablen zu ersetzen, um so die parallele Ausführbarkeit des Programms zu verbessern. Im Allgemeinen wird man nach Einführung der volatile-Variablen auch nicht immer vollständig auf das Locking verzichten können, sondern nur bei einem Teil der Zugriffe. Sind dies aber die Zugriffe, die in der Praxis am häufigsten vorkommen, so hat man das Wesentliche (nämlich eine Performanceverbesserung) schon erreicht.

Kommen wir zu unserem Beispiel. Es geht darum, ein privates Feld in einer Klasse nicht bei der Konstruktion, sondern beim ersten Zugriff zu initialisieren (lazy initialization):

public class MyClass {
    private MyField lazyField;
    ...

    public synchronized MyField getMyField() {
        if (lazyField == null)
            lazyField = new MyField( ... );

        return lazyField;
    }

    ...
}

Um fehlerhafte Mehrfach-Initialisierung auszuschließen, ist die getMyField-Methode synchronized, d.h. wir verwenden das mit this assoziierte Lock, um die Initialisierung zu schützen.

Erst einmal ist nichts gegen diese Implementierung einzuwenden: sie tut das, was sie soll, fehlerfrei. Zweifel bezüglich der Performance kommen einem aber, wenn man sich überlegt, dass das Lock im wesentlich nur für die Initialisierung benötigt wird. Nur beim allerersten Aufruf, wenn das MyField-Objekt erzeugt und seine Adresse im Feld lazyField abgelegt wird, kann eine Race Condition auftreten, die man per Synchronisation auflöst. Bei allen weiteren Aufrufen wird nur noch die Adresse abgefragt und zurückgegeben, wofür keine Synchronisation gebraucht wird.  Trotzdem wird das Lock bei jedem Aufruf von getMyField() wieder benutzt.

Welche Optimierungsmöglichkeiten haben wir? Die kritische Region können wir noch unwesentlich verkleinern und das return herausziehen. Um die Benutzung des Locks selbst kommen wir so einfach aber nicht herum.
 

Das Double-Check-Idiom

Doug Schmitt hat sich vor mehr als zwölf Jahren schon mal Gedanken zu diesem Problem gemacht und ist damals auf das Double-Check-Idiom gekommen (die Details zu seiner Veröffentlichung sowie zur Historie des Idioms finden sich im Insert ). Übertragen auf unser Ausgangsproblem sieht das Double-Check-Idiom so aus:
public class MyClass {
    private volatile MyField lazyField;
    ...

    public MyField getMyField() {
        if (lazyField == null) {  // Zeile 2
            synchronized (this) {
                if (lazyField == null) {
                    lazyField = new MyField( ... );
                }
            }
         }

        return lazyField;         // Zeile 8
    }

    ...
}

In der Methode getMyField() wird ohne jede Synchronisation auf das Feld lazyField zugegriffen. Da es volatile ist, sehen wir auch, ob andere Threads es bereits initialisiert haben (warum das so ist haben wir im letzten Artikel im Detail diskutiert / JMM4 /). Falls die Initialisierung bereits erfolgt ist, geben wir lazyField zurück und die Methode ist beendet. Und wenn die Initialisierung noch nicht erfolgt ist? Dann erfolgt sie in einem synchronized-Block. Dabei wird in dem Block noch einmal geprüft, ob lazyField immer noch null ist. Schließlich kann in der Zeit zwischen dem ersten Prüfen und dem Eintreten in den synchronized-Block die Initialisierung durch einen anderen Thread erfolgt sein.

Woher der Name ‚Double-Check-Idiom’ stammt, ist wohl offensichtlich: das relevante Feld lazyField wird zweimal geprüft. Dabei kommt im Englischen noch ein gewisser Sprachwitz zum tragen: double check bedeutet idiomatisch so etwas wie genau prüfen und meint damit nicht unbedingt die zweimalige Prüfung.

Aber zurück zum Technischen: trotz der zweimaligen Prüfung hat sich die Performance verbessert. Die zweite Prüfung erfolgt nämlich nur bei der Initialisierung; möglicherweise sogar in mehreren Threads, wenn diese gerade zur gleichen Zeit die Methode getMyField() zum ersten Mal aufrufen. Dieser minimale Performanceverlust bei der Initialisierung wird durch den völligen Verzicht auf Locking im weiteren Ablauf des Programms mehr als wettgemacht. Das gilt schon bei einer Ein-Kern- / Ein-Prozessor-Architektur. Bei einer Multi-Kern- / Multi-Prozessor-Architektur ist der Gewinn noch höher, da verschiedene Threads die Methode getMyField() (nach der Initialisierung) echt parallel ausführen können.

Was bedeutet das Beispiel im Allgemeinen? Die Verwendung von volatile kann helfen, die Performance bei konkurrierenden Threadzugriffen, die bisher mit Locks synchronisiert wurden, zu verbessern. Es gibt kein "Kochrezept" für das Vorgehen. Ein Tipp ist aber, dass man nicht versuchen muss, das Locking ganz zu vermeiden. Für die Performanceverbesserung reicht es auch, dass das Locking im Programmablauf signifikant weniger durchlaufen wird.

Eine Optimierung

Als wir das Konzept zu diesem Artikel zusammengestellt haben, ist zufällig gerade die neueste Ausgabe vom Joshua Blochs Effective Java (siehe / BLCH /) herausgekommen. Dort im Item 71 (Use lazy initialization judiciously) hat Joshua Bloch seine Version des Double-Check-Idioms vorgestellt. Diese Version lässt die Speichereffekte von volatile noch subtiler in die Implementierung einfließen; deshalb wollen wir sie hier auch diskutieren.
Auf unser Beispiel angewandt sieht Joshua Bloch's Implementierung so aus:
public class MyClass {
    private volatile MyField lazyField;
    ...

    public MyField getMyField() {
        MyField tmp = lazyField;
        if (tmp == null) {
            synchronized (this) {
                tmp = lazyField
                if (tmp == null) {
                    lazyField = tmp = new MyField( ... );
                }
            }
         }

        return tmp;
    }

    ...
}

Der offensichtliche Unterschied liegt in der zusätzlichen lokalen Variablen tmp. Welche Aufgabe hat sie?
 
Das Double-Check-Idiom und seine Geschichte
Fangen wir erst einmal beim Namen an, denn für den gibt es einige Varianten. Für den vorderen Teil gibt es Double-Check und Double-Checked, für den hinteren Namensbestandteil Idiom, Pattern und Locking. In Kombination ergeben sich damit sechs Möglichkeiten von: Double-Check-Idiom bis Double-Checked Locking. Wir haben, weil es am kürzesten ist, Double-Check-Idiom verwendet.

Unseres Wissen nach ist das Idiom zum ersten Mal von Doug Schmidt in seinem Editorial der März-Ausgabe des C++-Reports 1996 beschrieben worden worden. Der Originaltext befindet sich heute in Doug Schmidts Archiv (/ DCS /). 

Das Idiom ist dann, wie viele andere Programmiertechniken, von C++ nach Java übernommen worden. Irgendwann ist einigen schlauen Leuten (namentlich: David Bacon, Joshua Bloch, Jeff Bogda, Cliff Click, Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, John D. Mitchell, Kelvin Nilsen, Bill Pugh und Emin Gun Sirer) aufgefallen, dass das Verhalten von volatile Variablen, wie es in der damaligen Java Language Specification (JLS) beschrieben war, nicht ausreichend ist, um ein Funktionieren des Double-Check-Idioms in Java zu garantieren. Die Details sowie weitere nichtfunktionierende und funktionierende Varianten/Alternativen haben sie unter dem Titel The “Double-Checked Locking is Broken” Declaration (/ DCLB /) veröffentlicht.

Gleichzeitig hat sich die Java Community gefragt, ob dieses Problem nicht auf Grund eines Defizits in Java entstanden ist. Das heißt, sollte man Java nicht so ändern, dass so etwas wie das Double-Check-Idiom funktioniert? Aus diesen Überlegungen entstand dann der JSR 133: Java Memory Model and Thread Specification Revision (Spec Lead: Bill Pugh). Das Ergebnis dieses JSRs war dann das mit Java 5.0 neu eingeführte Memory Model, dessen Bedeutung für volatile wir im letzten Artikel ausführlich theoretisch diskutiert haben und dessen Bedeutung für die Praxis wir hier am Beispiel des Double-Check-Idioms aufzeigen. So schließt sich der Kreis.

Wir erinnern uns noch mal daran, was beim Lesen einer volatile Variable passiert (wir hatten das im letzten Artikel detailliert diskutiert / JMM4 /): der Wert der Variable kann nicht einfach aus dem lokalen Arbeitsspeicher eines Thread genommen werden, sondern muss neu aus dem Hauptspeicher gelesen werden, um sicher zu sein, dass Updates, die andere Threads möglicherweise gemacht haben, sichtbar werden.

In der ersten Implementierung des Double-Check-Idioms wird die volatile Variable lazyField im Nicht-Initialisierungsfall zweimal gelesen: einmal beim Vergleich (Zeile 2) und einmal beim return (Zeile 8). In beiden Fällen wird ein ‚Refresh’ vom Hauptspeicher gemacht. Der zweite ‚Refresh’ ist aber im Nicht-Initialisierungsfall überflüssig, da wir wissen, dass der Wert von lazyField sich nach der Initialisierung nicht mehr ändert. Das heißt, das Feld lazyField ist sowas wie ‚semi final’. Es wird genau einmal gesetzt. Da es sich um eine lazy initialization handelt, erfolgt das Setzen aber nicht im Konstruktor sondern in getMyField(). Deshalb kann das Feld nicht wirklich als final deklariert werden. Trotzdem ändert es sich im weiteren Ablauf des Programms nicht mehr. Da Java-Compiler und Java-Laufzeitsystem nichts von dieser ‚semi final’ Eigenschaft wissen, können sie keine Optimierung vornehmen und auf den zweiten ‚Refresh’ vom Hauptspeicher nicht verzichten. Das bedeutet, wir müssen die Optimierung selbst machen. So kommt dann die Variante von Joshua Bloch heraus. Dort wird im Nicht-Initialisierungsfall nur einmal lesend auf lazyField zugegriffen, nämlich bei der Zuweisung an die lokale Variable tmp. Die Performance-Verbesserung auf Grund dieser Optimierung beträgt laut Joshua Bloch rund 25%.

Noch zwei Kommentare zum Double-Check-Idiom:

Wenn man sich die drei verschiedenen Implementierungen ansieht:

  • Lock ohne volatile
  • Double-Check-Idiom, d.h. Lock und volatile
  • optimiertes Double-Check-Idiom, d.h. Lock, volatile und lokale Variable
dann fällt auf, dass es immer mehr Code wird, je performanter die Lösung ist. Dabei stört nicht so sehr die größere Menge an Code, als vielmehr, dass er auch immer unintuitiver wird. Aber damit muss man sich wohl heute in Java abfinden; das Performance-Modell von Java ist nun mal an einigen Stellen in hohem Maße unintuitiv.

Vollständigkeitshalber sei noch erwähnt, dass das Double-Check-Idiom nicht die beste und einzige Lösung ist, wenn es darum geht, ein static Feld lazy zu initialisieren. Solche verzögerten Initialisierungen von statischen Feldern treten zum Beispiel im Zusammenhang mit Singletons auf. Hier eignet sich das Holder-Class-Idiom besser. Es basiert im wesentlichen darauf, das Problem der einmaligen Initialisierung an das Laufzeitsystem der JVM zu delegieren. Wir wollen diese Lösung hier aber nicht diskutieren, weil sie überhaupt gar nichts mit dem Thema volatile und dem Java Memory Model zu tun hat. Details zum HolderClass-Idiom finden sich aber auch im Item 71 von Joshua Blochs Buch.

Single-Check-Idiome

Stattdessen wollen wir uns noch zwei Varianten des Double-Check-Idioms ansehen, an denen man Effekte des Memory Models diskutieren kann: Single-Check-Idiom und Racy-Single-Check-Idiom (beide sind auch in Joshua Bochs Buch erwähnt). Es geht dabei darum, ob und unter welchen Umständen man die Datensynchronisation verringern kann, d.h. den synchronized-Block und das volatile weglassen, um so vielleicht die Performance zu verbessern.

Das Single-Check-Idiom sieht so aus, wobei wir hier die Version mit nur einem ‚Refresh’ unter Verwendung einer temporären Variablen betrachten:

public class MyClass {
    private volatile MyField lazyField;
    ...

    public MyField getMyField() {
        MyField tmp = lazyField;
        if (tmp == null)
            lazyField = tmp = new MyField();

        return tmp;
    }

    ...
}

Da der synchronized-Block fehlt, kann es hierbei natürlich zu mehrfachen Initialisierungen kommen. Das heißt, für mehrere Threads könnte tmp == null sein und jeder von ihnen würde dann das Feld lazyField initialisieren. Das Objekt, dessen Referenz als letztes an lazyField zugewiesen wird, ist dann das Objekt, welches anschließend allen Threads sichtbar ist, da das Feld lazyField als volatile deklariert ist.

Diese Mehrfachinitialisierung muss aber kein Problem sein. Es gibt Situationen, wo dies toleriert werden kann. Zum Beispiel, wenn in jedem Thead das gleiche Objekt erzeugt wird und MyField ein nicht veränderbarer (immutable) Typ ist.

Von der Performance her ist das Single-Check-Idiom dem Double-Check-Idiom nicht wirklich überlegen. Da sich beide nur während der Initialisierung unterscheiden, dürften die Performanceunterschiede für den gesamten Programmablauf nicht signifikant sein. Für den Fall von Thread-Kollisionen bei der Initialisierung haben beide Lösungen ihre spezifischen Nachteile, die sich schlecht gegeneinander aufrechnen lassen:

  • Beim Single-Check-Idiom werden überflüssige Objekte konstruiert, was zusätzliche CPU-Zeit kostet.
  • Die Initialisierung beim Double-Check-Idiom wird sequentialisiert und damit skaliert sie nicht so gut.
Vorteil des Single-Check-Idiom ist, dass es knapper in der Implementierung ist und zusätzlich dokumentiert, dass Mehrfach-Initialisierungen hier toleriert werden können .

Bei der zweiten Variante des des Double-Check-Idioms, dem Racy-Single-Check-Idiom, fällt nicht nur der synchronized-Block weg, sondern auch noch das volatile. Ein Beispiel für das  Racy-Single-Check-Idiom  (mit int) sieht dann so aus:

public class MyClass {
    private int lazyField;
    ...

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

        return lazyField;
    }

    ...
}

Da das lazyField nicht mehr volatile ist, braucht man auch keine temporäre Variable mehr zur Optimierung. Von der Performance her ist diese Implementierung optimal, da hier gar keine expliziten Speichereffekte mehr getriggert werden.  Sie ist aber nur sehr eingeschränkt verwendbar.

Hier ist nämlich nicht mehr gesichert, dass ein Thread die Initialisierung sieht, die von einem anderen Thread zuvor durchgeführt wurde. Ausgeschlossen ist es aber auch nicht, da die Sichtbarkeit durch andere Effekte hergestellt werden kann, zum Beispiel durch benutzerseitige Synchronisation.  Das wäre der Fall, wenn der Aufruf der Methode getMyField() in einem synchronized-Block erfolgt, der von beiden Threads durchlaufen wird.  Dann passiert die Synchronisation von Außen und nicht in der Klasse MyClass selbst.  In so einer Situation ist das Racy-Single-Check-Idiom durchaus sinnvoll.

Natürlich kann es auch beim Racy-Single-Check-Idiom (wie beim Single-Check-Idiom) passieren, dass das Feld mehrmals initialisiert wird, weil es keine Synchronisation mehr gibt.  Beim Racy-Single-Check-Idiom gibt es aber noch zwei andere Probleme.

Wenn das Feld, das lazy initialisiert werden soll, kein int-Wert ist, sondern vom Typ long oder double, dann ist der Zugriff darauf nicht atomar.  Es könnte also passieren, dass das Lesen des lazy-Felds einen sinnlosen Wert liefert.  Andere Probleme gibt es, wenn das Feld eine Referenz auf ein Objekt ist.  Dann ist zwar der Zugriff auf die Adresse atomar, aber es gibt keine Garantie, dass der lesende Thread das referenzierte Objekt in einem konsistenten Zustand sieht.  Schließlich ist ohne Synchronisation oder volatile nicht gewährleistet, dass die Felder des Objekts jemals sichtbar gemacht werden.

Kann man bei all den Einschränkungen mit dem Racy-Single-Check-Idiom überhaupt etwas anfangen? In der Theorie macht es Sinn, sich das Racy-Single-Check-Idiom zur Abgrenzung vom Single-Check-Idiom einmal vor Augen zu führen. In der Praxis sind Anwendungsfälle, bei denen das Racy-Single-Check-Idiom sinnvoll zum Einsatz kommt, eher selten.  Es gibt aber Anwendungsfälle, zum Beispiel den HashCode im String.  Da wird das int-Feld in der Klasse java.lang.String, das den Hashcode des String cacht, mit dem Racy-Single-Check-Idiom in der Methode hashCode() lazy-initialisiert - ohne Synchronisation und ohne volatile.

Hier ist der relevante Auszug aus der Implementierung der Klasse java.lang.String:

public final class String
{
    /** Cache the hash code for the string */
    private int hash; // Default to 0

    public String() {
 this.offset = 0;
 this.count = 0;
 this.value = new char[0];
    }

    public int hashCode() {
 int h = hash;
 if (h == 0) {
     int off = offset;
     char val[] = value;
     int len = count;

            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
    }
}

In diesem Anwendungsfall macht es keine Probleme, dass der eine Thread, der hashCode() aufruft, u.U. nicht sehen kann, was ein anderer Thread zuvor in dem hash-Feld abgelegt hat.  Da die HashCode-Berechnung sowieso immer dasselbe Ergebnis liefert, ist es egal, ob ein Thread den Wert sieht, den er gerade selber ausgerechnet hat, oder den Wert, den zuvor ein anderer Thread berechnet hat. Es macht auch nichts, dass das Feld eventuell zweimal initialisiert wird, weil sich der HashCode eines Strings nicht ändern kann.  Also kann es nicht passieren, dass die zweite Initialisierung einen vom Default- und Initialwert abweichenden "aktuellen" Wert des hash-Feldes überschreibt.  Das Racy-Single-Check-Idiom funktioniert hier natürlich nur, weil java.lang.String ein unveränderlicher (immutable) Typ ist; sonst wäre der HashCode nämlich nicht immer gleich und dann wäre eine Lösung ohne Synchronisation und ohne volatile falsch.

In einem der nächsten Beitrag wollen wir uns dann einen weiteren Anwendungsfall für das Racy-Single-Check-Idiom ansehen. Wie wir oben schon erwähnt habe, ist Racy-Single-Check-Idiom  problematisch, wenn das lazy-initialisierte Feld eine Referenz auf ein Objekt ist.  Das gibt aber nur, wenn das referenzierte Objekt veränderlich ist.  Bei der Verwendung eines unveränderlichen (immutable) Typs ist die Initialisierung ohne Synchronisation und ohne volatile durchaus sinnvoll.  Dazu muss der unveränderliche Typ aber korrekt implementiert sein und dazu werden wiederum die Speichergarantien für final-Felder gebraucht - und das wollen wir uns in den nächsten Beiträgen genauer ansehen.

Zusammenfassung

In diesem Beitrag haben wir die Speichereffekte von volatile Variablen am konkreten Beispiel des Double-Check-Idioms und einiger seiner Varianten diskutiert. In unserem nächsten Beitrag wollen wir uns noch einmal typische Anwendungsidiome für volatile ansehen.
 

Literaturverweise und weitere Informationsquellen

/ DCS / Reality Check
Douglas C. Schmidt
URL: http://www.cs.wustl.edu/~schmidt/editorial-3.html
/ DCLB / The “Double-Checked Locking is Broken” Declaration
David Bacon, Joshua Bloch, Jeff Bogda, Cliff Click, Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, John D. Mitchell, Kelvin Nilsen, Bill Pugh Emin Gun Sirer
URL: http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
/ BLCH / Effective Java, 2nd Ed.
Joshua Bloch
Addison-Wesley, 2008

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

 

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/41.JMM-DoubleCheck/41.JMM-DoubleCheck.html  last update: 22 Mar 2015