Warum wird flüchtig in doppelt überprüften Verriegelungen verwendet


77

Aus dem Head First- Entwurfsmusterbuch wurde das Singleton-Muster mit doppelt überprüfter Verriegelung 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 volatileverwendet wird. Besiegt die volatileVerwendung nicht den Zweck der Verwendung von doppelt überprüftem Sperren, dh Leistung?


6
Ich dachte, die doppelt überprüfte Verriegelung sei defekt. Hat jemand sie repariert?
David Heffernan

4
Für das, was es wert ist, fand ich Head First Design Patterns ein schreckliches Buch, aus dem man lernen kann. Wenn ich zurückblicke, ist es vollkommen sinnvoll, jetzt, wo ich die Muster anderswo gelernt habe, aber zu lernen, ohne die Muster zu kennen, hat es wirklich nicht seinen Zweck erfüllt. Aber es ist sehr beliebt, vielleicht war es nur ich, der dicht war. :-)
corsiKa

@DavidHeffernan Ich habe gesehen, dass dieses Beispiel als die einzige Möglichkeit verwendet wird, der JVM für die Ausführung der DCL zu vertrauen.
Nathan Feger

FWIW, auf einem x86-System soll ein flüchtiges Read-Read zu einem No-Op führen. Tatsächlich ist die einzige Operation, die einen Zaun für die Speicherkonsistenz erfordert, ein flüchtiges Schreiben-Lesen. Wenn Sie den Wert also wirklich nur einmal schreiben, sollte dies nur minimale Auswirkungen haben. Ich habe noch niemanden gesehen, der dies tatsächlich bewertet und denke, das Ergebnis wäre interessant!
Tim Bender

@ DavidHeffernan aus allen praktischen Gründen ist es immer noch kaputt. Es ist besser mit volatile(wie in "wird dein Programm nicht vermasseln"), aber dann gewinnst du nicht wirklich viel.
alf

Antworten:


70

Eine gute Quelle, um zu verstehen, warum dies volatileerforderlich ist, ist das JCIP- Buch. Wikipedia hat auch eine anständige Erklärung für dieses Material.

Das eigentliche Problem besteht darin, dass Thread Amöglicherweise ein Speicherplatz zugewiesen wird, instancebevor die Erstellung abgeschlossen ist instance. Thread Bwird diese Zuordnung sehen und versuchen, sie zu verwenden. Dies führt zu einem Thread BFehler, da eine teilweise erstellte Version von verwendet wird instance.


1
OK, es sieht so aus, als ob eine neue Implementierung von volatile die Speicherprobleme mit DCL behoben hat. Was ich immer noch nicht verstehe, ist die Auswirkung der Verwendung von Volatile hier auf die Leistung. Nach dem, was ich gelesen habe, ist flüchtig so langsam wie synchronisiert. Warum also nicht einfach den gesamten Methodenaufruf getInstance () synchronisieren?
toc777

5
@ toc777 volatileist langsamer als ein normales Feld . Wenn Sie nach Leistung suchen, entscheiden Sie sich für ein Muster der Halterklasse. volatileist nur hier, um zu zeigen, dass es einen Weg gibt , das gebrochene Muster zum Laufen zu bringen. Es ist eher eine Codierungsherausforderung als ein echtes Problem.
alf

6
@ Tim gut, ein Singleton in XML ist immer noch ein Singleton; Das Verstehen des Laufzeitstatus einer App wird durch die Verwendung von DI nicht einfacher. kleinere Codeeinheiten scheinen einfacher zu sein, auf Kosten einer starken Strukturierung aller Einheiten, um der DI-Sprache zu entsprechen (eine gute Sache, die manche sagen könnten). Die Anschuldigung gegen Singleton ist nicht fair, sie verwechselt API mit impl - Foo.getInstance()ist nur ein Ausdruck, um irgendwie ein Foo zu bekommen, es unterscheidet sich nicht von @Inject Foo foo; In beiden Fällen ist die Site, die ein Foo anfordert, unabhängig davon, welches Foo zurückgegeben wird und wie, in beiden Fällen sind statische und Laufzeitabhängigkeiten gleich.
Unbestreitbarer

2
@irreputable Du weißt, was lustig ist, dass ich zu dem Zeitpunkt, als wir diesen Austausch hatten, Spring noch nie benutzt hatte und mich nicht auf Spring's Wonky DI bezog. Die wirkliche Gefahr bei Singleton als static factoryist die Versuchung, es tief im Code zu nennen, der die getInstance()Methode nicht kennen und stattdessen die Bereitstellung einer Instanz verlangen sollte.
Tim Bender

1
The real problem is that Thread A may assign a memory space for instance before it is finished constructing instance. Wie kann das volatile gelöst werden?
Shaoyihe

21

Wie von @irreputable angegeben, ist flüchtig nicht teuer. Auch wenn es teuer ist, sollte der Konsistenz Vorrang vor der Leistung eingeräumt werden.

Es gibt noch 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();
    }
}

Quellartikel : Initialization-on-Demand_holder_idiom aus Wikipedia

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

Da die Klasse keine staticzu initialisierenden Variablen hat, wird die Initialisierung trivial abgeschlossen.

Die darin enthaltene statische Klassendefinition LazyHolderwird erst initialisiert, wenn die JVM feststellt, dass LazyHolder ausgeführt werden muss.

Die statische Klasse LazyHolderwird nur ausgeführt, wenn die statische Methode getInstancefür die Klasse Singleton aufgerufen wird. Wenn dies zum ersten Mal geschieht, lädt und initialisiert die JVM die LazyHolderKlasse.

Diese Lösung ist threadsicher, ohne dass spezielle Sprachkonstrukte (dh volatileoder synchronized) erforderlich sind .


11

Nun, es gibt keine doppelt überprüfte Sperre für die Leistung. Es ist ein gebrochenes Muster.

Emotionen beiseite zu lassen, volatileist hier, weil ohne sie zum Zeitpunkt des Ablaufs des zweiten Threads der instance == nullerste Thread möglicherweise noch nicht konstruiert new Singleton()ist: Niemand verspricht, dass die Erstellung des Objekts erfolgt - vor der Zuweisung instancefür einen Thread, außer demjenigen, der das Objekt tatsächlich erstellt.

volatilewiederum gründet geschieht zuvor Beziehung zwischen Lese- und Schreibvorgänge, und fixiert das gebrochene Muster.

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


1
hi @ alf , Gemäß der Definition "passiert vor", nachdem ein Thread die Sperre aufgehoben hat, erwirbt ein anderer Thread die Sperre, und dieser kann die vorherige Änderung sehen. Wenn ja, denke ich nicht, dass das flüchtige Schlüsselwort benötigt wird. Können Sie es genauer erklären
Qin Dong Liang

Es gibt jedoch keine Sperrenerfassung für den Fall, dass der zweite Thread nur das äußere Wenn trifft, also keine Bestellung.
alf

Hallo @alf, versuchen Sie also festzustellen, dass, wenn der erste Thread die Instanz innerhalb eines synchronisierten Blocks erstellt, diese Instanz für den zweiten Thread möglicherweise immer noch null ist, da der Cache fehlschlägt, und sie erneut instanziiert, wenn die Instanz nicht flüchtig ist? Können Sie bitte klarstellen ?
Aarish Ramesh

1
@AarishRamesh, nicht null; jeder Staat überhaupt. Es gibt zwei Operationen: Zuweisen der Adresse zur instanceVariablen und die tatsächliche Erstellung des Objekts an dieser Adresse. Sofern die Synchronisierung nicht erzwungen wird, z. B. ein volatileZugriff oder eine explizite Synchronisierung, kann der zweite Thread diese beiden Ereignisse außer Betrieb setzen.
alf

2

Wenn Sie es nicht hätten, könnte ein zweiter Thread in den synchronisierten Block gelangen, nachdem der erste ihn auf null gesetzt hat, und Ihr lokaler Cache würde immer noch denken, dass er null ist.

Der erste dient nicht der Korrektheit (wenn Sie richtig wären, würde er sich selbst besiegen), sondern der Optimierung.


1
Gemäß der Definition "passiert vor", nachdem ein Thread die Sperre aufgehoben hat, erwirbt ein anderer Thread die Sperre, und dieser kann die vorherige Änderung sehen. Wenn ja, denke ich nicht, dass das flüchtige Schlüsselwort benötigt wird. Können Sie es genauer erklären
Qin Dong Liang

@QinDongLiang Wenn die Variable nicht flüchtig ist, verwendet der zweite Thread möglicherweise einen zwischengespeicherten Wert auf seinem eigenen Stapel. Wenn es volatil ist, muss es zur Quelle zurückkehren, um den richtigen Wert zu erhalten. Natürlich muss es dies bei jedem Zugriff tun und kann daher einen Leistungseinbruch haben (aber seien wir ehrlich, es sei denn, es befindet sich in einer überkritischen Schleife, es ist wahrscheinlich nicht das Schlimmste in Ihrem System ...)
corsiKa

2

Das Deklarieren der Variablen als volatilegarantiert, dass alle Zugriffe darauf tatsächlich ihren aktuellen Wert aus dem Speicher lesen.

Ohne volatilekann der Compiler die Speicherzugriffe auf die Variable optimieren (z. B. den Wert in einem Register behalten), sodass nur die erste Verwendung der Variablen den tatsächlichen Speicherort liest, an dem sich die Variable befindet. 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 iftestet die zweite Anweisung eine veraltete Kopie des Wertes der Variablen.


4
-1 Ich verliere heute meine Reputationspunkte :) Der wahre Grund ist, dass es einen Speichercache gibt, der als lokaler Speicher des Threads modelliert ist. Die Reihenfolge, in der der lokale Speicher in den Hauptspeicher geleert wird, ist undefiniert - es sei denn, Sie haben Vor- Beziehungen, z volatile. B. mithilfe von . Register haben nichts mit unvollständigen Objekten und DCL-Problemen zu tun.
alf

3
Ihre Definition von volatileist zu eng - wenn das alles flüchtig gewesen wäre, hätte das doppelt überprüfte Sperren in <Java5 gut funktioniert. volatileführt eine Speicherbarriere ein, die eine bestimmte Neuordnung illegal macht - ohne diese wäre es immer noch unsicher, selbst wenn wir niemals veraltete Werte aus dem Speicher lesen. Edit: alf hat mich geschlagen, hätte mir keinen schönen Tee
holen sollen

1
@TimBender Wenn Singleton einen veränderlichen Status enthält , hat das Löschen nichts mit einem Verweis auf den Singleton selbst zu tun (nun, es gibt eine indirekte Verknüpfung, da der Zugriff auf einen volatlieVerweis auf einen Singleton Ihren Thread dazu führt, den Hauptspeicher erneut zu lesen - aber es ist ein sekundärer Effekt , nicht die Ursache eines Problems :))
alf

@alf, du hast recht. Und tatsächlich hilft es nicht, die Instanz flüchtig zu machen, wenn der Status im Inneren veränderlich ist, da der Flush nur dann erfolgt, wenn die Referenz selbst geändert wird (wie das flüchtige Erstellen von Arrays / Listen nichts für den Inhalt bedeutet). Kreide es zu einem Hirnfurz.
Tim Bender

Gemäß der Definition "passiert vor", nachdem ein Thread die Sperre aufgehoben hat, erwirbt ein anderer Thread die Sperre, und dieser kann die vorherige Änderung sehen. Wenn ja, denke ich nicht, dass das flüchtige Schlüsselwort benötigt wird. Können Sie es genauer erklären @ Tim Bender
Qin Dong Liang

1

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

Sie können einen Test entwerfen, der getInstance()in einer engen Schleife aufgerufen wird, um die Auswirkungen eines flüchtigen Lesevorgangs zu beobachten. Dieser Test ist jedoch nicht realistisch. In einer solchen Situation ruft der Programmierer normalerweise getInstance()einmal auf und speichert die Instanz für die Dauer der Verwendung zwischen.

Ein weiteres Impl ist die Verwendung eines finalFeldes (siehe Wikipedia). Dies erfordert einen zusätzlichen Lesevorgang, der teurer werden kann als die volatileVersion. Die finalVersion kann in einer engen Schleife schneller sein, jedoch ist dieser Test wie zuvor argumentiert umstritten.


0

Das doppelt überprüfte Sperren ist eine Technik, die verhindert, dass eine weitere Instanz von Singleton erstellt wird, wenn die getInstanceMethode in einer Multithreading-Umgebung aufgerufen wird.

Passt auf

  • Die Singleton-Instanz wird vor der Initialisierung zweimal überprüft.
  • Der synchronisierte kritische Abschnitt wird erst nach der ersten Überprüfung der Singleton-Instanz verwendet, um die Leistung zu verbessern.
  • volatileSchlüsselwort in der Deklaration des Instanzmitglieds. Dadurch wird der Compiler angewiesen, immer aus dem Hauptspeicher und nicht aus dem CPU-Cache zu lesen und in diesen zu schreiben. Bei der volatileVariablengarantie, die vor der Beziehung erfolgt, erfolgt der gesamte Schreibvorgang vor dem Lesen der Instanzvariablen.

Nachteile

  • Da das volatileSchlüsselwort ordnungsgemäß funktionieren muss, ist es nicht mit Java 1.4 und niedrigeren Versionen kompatibel. Das Problem besteht darin, dass bei einem Schreibvorgang außerhalb der Reihenfolge möglicherweise die Instanzreferenz zurückgegeben werden kann, bevor der Singleton-Konstruktor ausgeführt wird.
  • Leistungsproblem aufgrund des Cache für flüchtige Variablen.
  • Die Singleton-Instanz wird vor der Initialisierung zweimal überprüft.
  • Es ist ziemlich ausführlich und macht es schwierig, den Code zu lesen.

Es gibt mehrere Realisierungen von Singleton-Mustern mit jeweils Vor- und Nachteilen.

  • Eifriges Laden von Singleton
  • Doppelter Sperr-Singleton
  • Initialisierung-on-Demand-Inhabersprache
  • Der auf Enum basierende Singleton

Detaillierte Beschreibung Jeder von ihnen ist zu ausführlich, deshalb habe ich nur einen Link zu einem guten Artikel eingefügt - Alles, was Sie über Singleton wissen wollen

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.