it-swarm.com.de

Warum wird flüchtig bei doppelt geprüften Verriegelungen verwendet?

Aus dem Buch " Head First Design Patterns" wurde das Singleton-Pattern mit doppeltem Check-Lock wie folgt implementiert: 

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Ich verstehe nicht, warum volatile verwendet wird. Verweigert die Verwendung von volatile nicht den Zweck der Verwendung von doppelt geprüften Sperren, d. H. Der Leistung?

59
toc777

Eine gute Ressource, um zu verstehen, warum volatile benötigt wird, stammt aus dem Buch JCIP . Wikipedia hat eine anständige Erklärung auch dieses Materials.

Das eigentliche Problem ist, dass Thread A einen Speicherplatz für instance zuweisen kann, bevor die Erstellung von instance abgeschlossen ist. Thread B wird diese Zuweisung sehen und versuchen, sie zu verwenden. Dies führt dazu, dass Thread B fehlschlägt, da eine teilweise konstruierte Version von instance verwendet wird.

58
Tim Bender

Wie von @irreputable zitiert, ist volatil nicht teuer. Selbst wenn dies teuer ist, sollte Konsistenz Vorrang vor Leistung haben. 

Es gibt einen sauberen, eleganten Weg für Lazy Singletons.

public final class Singleton {
    private Singleton() {}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

Quelltext: Initialisierung-on-demand_holder_idiom aus Wikipedia

In der Softwareentwicklung ist das Idiom "Initialization on Demand Holder" (Entwurfsmuster) ein faul geladener Singleton. In allen Java-Versionen ermöglicht das Idiom eine sichere, sehr zeitsparende Initialisierung mit guter Leistung

Da die Klasse keine static-Variablen zum Initialisieren besitzt, wird die Initialisierung trivial abgeschlossen. 

Die statische Klassendefinition LazyHolder darin wird nicht initialisiert, bis die JVM feststellt, dass LazyHolder ausgeführt werden muss.

Die statische Klasse LazyHolder wird nur ausgeführt, wenn die statische Methode getInstance für die Klasse Singleton aufgerufen wird. Wenn dies der Fall ist, lädt und speichert die JVM die LazyHolder-Klasse.

Diese Lösung ist threadsicher, ohne dass spezielle Sprachkonstrukte erforderlich sind (d. H. volatile oder synchronized).

14
Ravindra babu

Nun, es gibt keine doppelt gesicherten Sperren für die Leistung. Es ist ein broken Muster.

Abgesehen von den Emotionen ist volatile hier, weil ohne den Zeitpunkt, zu dem der zweite Thread instance == null passiert, der erste Thread new Singleton() noch nicht erstellt werden kann: Niemand verspricht, dass die Erstellung des Objekts happen-before für einen anderen Thread als instance erstellt wird tatsächlich das Objekt erstellen.

volatile stellt wiederum die Beziehung happen-before zwischen Lese- und Schreibvorgängen her und behebt das unterbrochene Muster.

Wenn Sie Leistung suchen, verwenden Sie stattdessen die innere statische Klasse des Halters.

7
alf

Wenn Sie es nicht haben, könnte ein zweiter Thread in den synchronisierten Block geraten, nachdem der erste auf null gesetzt wurde, und der lokale Cache würde immer noch denken, dass er null war.

Die erste ist nicht für die Richtigkeit (wenn Sie richtig wären, dass es sich selbst besiegen würde), sondern eher für die Optimierung.

1
corsiKa

Ein flüchtiger Lesevorgang ist an sich nicht wirklich teuer. 

Sie können einen Test entwerfen, um getInstance() in einer engen Schleife aufzurufen, um die Auswirkungen eines flüchtigen Lesens zu beobachten. dieser Test ist jedoch nicht realistisch; In einer solchen Situation würde der Programmierer normalerweise getInstance() einmal aufrufen und die Instanz für die Dauer der Verwendung zwischenspeichern.

Ein anderes impl ist die Verwendung eines final-Feldes (siehe Wikipedia). Dies erfordert einen zusätzlichen Lesevorgang, der möglicherweise teurer wird als die volatile-Version. Die final-Version ist möglicherweise in einer engen Schleife schneller, dieser Test ist jedoch wie zuvor argumentiert.

1
irreputable

Durch das Deklarieren der Variablen als volatile wird sichergestellt, dass alle Zugriffe auf sie tatsächlich ihren aktuellen Wert aus dem Speicher lesen.

Ohne volatile kann der Compiler die Speicherzugriffe wegoptimieren und seinen Wert in einem Register behalten, sodass nur die erste Verwendung der Variablen den tatsächlichen Speicherplatz liest, der die Variable enthält. Dies ist ein Problem, wenn die Variable zwischen dem ersten und dem zweiten Zugriff von einem anderen Thread geändert wird. Der erste Thread hat nur eine Kopie des ersten (vormodifizierten) Werts, daher testet die zweite if-Anweisung eine veraltete Kopie des Variablenwerts.

0
David R Tribble