wake-up-neo.com

Java doppelt gesperrtes Sperren

Ich bin kürzlich auf einen Artikel gestoßen, in dem das doppelt geprüfte Sperrmuster in Java und seine Fallstricke behandelt wurden, und ich frage mich, ob eine Variante dieses Musters, die ich seit Jahren verwende, problematisch ist.

Ich habe mir viele Beiträge und Artikel zu diesem Thema angesehen und die möglichen Probleme mit dem Bezug eines teilweise konstruierten Objekts verstanden. Soweit ich das beurteilen kann, glaube ich nicht denke meine Implementierung unterliegt diesen Probleme. Gibt es Probleme mit dem folgenden Muster?

Und wenn nicht, warum benutzen die Leute es nicht? Ich habe es in keiner der Diskussionen, die ich zu diesem Thema gesehen habe, empfohlen empfohlen.

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}
37
Jim

Doppelte Überprüfung der Verriegelung ist gebrochen . Da initialized ein Grundelement ist, muss es möglicherweise nicht flüchtig sein, damit es funktioniert. Es gibt jedoch nichts, das die Initialisierung für den nicht-syncronisierten Code als wahr betrachtet, bevor die Instanz initialisiert wird.

BEARBEITEN: Um die obige Antwort zu klären, wurde in der ursprünglichen Frage nach der Verwendung eines Boolean-Befehls zum Steuern der Double Check-Sperre gefragt. Ohne die Lösungen im obigen Link funktioniert es nicht. Sie können die Sperre tatsächlich überprüfen, um einen booleschen Wert festzulegen, aber Sie haben immer noch Probleme mit der Neuordnung von Anweisungen, wenn Sie die Klasseninstanz erstellen. Die vorgeschlagene Lösung funktioniert nicht, da die Instanz möglicherweise nicht initialisiert wird, nachdem Sie den initialisierten Booleschen Wert im nicht synchronisierten Block als "true" sehen.

Die richtige Lösung für das doppelte Prüfen von Sperren besteht darin, entweder flüchtig (im Instanzfeld) zu verwenden und den initialisierten Boolean zu vergessen und sicher zu sein, JDK 1.5 oder höher zu verwenden, oder ihn in einem abschließenden Feld zu initialisieren, wie im verknüpften beschrieben Artikel und Toms Antwort, oder einfach nicht verwenden.

Sicherlich scheint das gesamte Konzept eine riesige verfrühte Optimierung zu sein, es sei denn, Sie wissen, dass Sie eine Menge Threads bekommen werden, wenn Sie dieses Singleton erhalten, oder Sie haben die Anwendung profiliert und dies als einen Hot Spot gesehen.

24
Yishai

Das würde funktionieren, wenn initializedvolatilewäre. Genau wie bei synchronizedhaben die interessanten Auswirkungen von volatilenicht so sehr mit der Referenz zu tun, als was wir über andere Daten sagen können. Das Einrichten des Feldes instanceund des Objekts Testist zwingend auf zufälligesSchreiben in initializedname__.) Wenn der zwischengespeicherte Wert über den Kurzschluss verwendet wird, wird der initializeread passiert-beforeLesen von instancename_ und _ Über den Verweis erreicht. Es gibt keinen signifikanten Unterschied, wenn ein separates initializedname__-Flag verwendet wird (es sei denn, dies verursacht noch mehr Komplexität im Code).

(Die Regeln für finalname__-Felder in Konstruktoren für eine unsichere Veröffentlichung sind etwas unterschiedlich.)

In diesem Fall sollte der Fehler jedoch selten angezeigt werden. Die Chancen, bei der ersten Verwendung in Schwierigkeiten zu geraten, sind gering und es ist ein nicht wiederholtes Rennen.

Der Code ist zu kompliziert. Sie könnten es einfach schreiben als:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}
16

Das zweifach geprüfte Sperren ist in der Tat gebrochen, und die Lösung des Problems ist tatsächlich einfacher zu implementieren, als es in diesem Code der Fall ist - verwenden Sie einfach einen statischen Initialisierer.

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

Es wird garantiert, dass ein statischer Initialisierer ausgeführt wird, wenn die JVM zum ersten Mal die Klasse lädt, und bevor die Klassenreferenz an einen beliebigen Thread zurückgegeben werden kann. Dies macht sie inhärent threadsicher. 

12
matt b

Dies ist der Grund, warum die doppelte überprüfte Verriegelung unterbrochen wird.

Synchronize garantiert, dass nur ein Thread einen Codeblock eingeben kann. Es kann jedoch nicht garantiert werden, dass innerhalb eines synchronisierten Abschnitts vorgenommene Variablenänderungen für andere Threads sichtbar sind. Nur die Threads, die den synchronisierten Block betreten, können die Änderungen garantiert sehen. Dies ist der Grund, warum die doppelte überprüfte Verriegelung aufgehoben wird - sie ist auf der Leserseite nicht synchronisiert. Der Lesethread kann sehen, dass der Singleton nicht null ist, aber Singleton-Daten möglicherweise nicht vollständig initialisiert (sichtbar) sind. 

Die Bestellung erfolgt über volatile. volatile garantiert die Reihenfolge, zum Beispiel das Schreiben in ein flüchtiges statisches Feld mit Singleton garantiert, dass das Schreiben in das Einzelobjekt vor dem Schreiben in das flüchtige statische Feld abgeschlossen wird. Das Erstellen eines Singleton von zwei Objekten wird nicht verhindert, dies wird durch Synchronisieren bereitgestellt.

Letzte statische Klassenfelder müssen nicht flüchtig sein. In Java kümmert sich das JVM um dieses Problem. 

Siehe meinen Beitrag, eine Antwort auf Singleton-Muster und gebrochenes, doppelt geprüftes Sperren in einer realen Java-Anwendung , wobei ein Beispiel für ein Singleton in Bezug auf doppelt geprüftes Sperren dargestellt wird, das zwar klug aussieht, aber es ist gebrochen.

5

Double-checked Locking ist das Anti-Pattern.

Lazy Initialization Holder Class ist das Muster, das Sie sich ansehen sollten.

Trotz so vieler anderer Antworten dachte ich, ich sollte antworten, denn es gibt immer noch keine einfache Antwort, die besagt, warum DCL in vielen Kontexten gebrochen wird, warum es unnötig ist und was Sie stattdessen tun sollten. Ich verwende also ein Zitat aus Goetz: Java Concurrency In Practice, das für mich die letzte Erklärung in seinem letzten Kapitel über das Java-Speichermodell liefert.

Es geht um die sichere Veröffentlichung von Variablen:

Das eigentliche Problem bei DCL ist die Annahme, dass das Schlimmste, was beim Lesen einer Shared-Objekt-Referenz ohne Synchronisation passieren kann, darin liegt, irrtümlich einen veralteten Wert (in diesem Fall null) zu sehen. In diesem Fall gleicht das DCL-Idiom dieses Risiko aus, indem es bei gehaltener Sperre erneut versucht. Der schlimmste Fall ist jedoch erheblich schlimmer. Es ist möglich, einen aktuellen Wert der Referenz, aber nicht mehr gültige Werte für den Zustand des Objekts zu sehen. Dies bedeutet, dass das Objekt möglicherweise in einem ungültigen oder falschen Zustand ist. 

Nachfolgende Änderungen in der JMM (Java 5.0 und höher) haben DCL zum Funktionieren gebracht, wenn die Ressourcen flüchtig gemacht werden. Die Auswirkungen auf die Leistung sind gering, da flüchtige Lesevorgänge normalerweise nur geringfügig teurer sind als nichtflüchtige Lesevorgänge.

Dies ist jedoch ein Idiom, dessen Nutzen weitgehend vergangen ist - die Kräfte, die ihn motiviert haben (langsame unkontrollierte Synchronisation, langsamer Start der JVM), sind nicht mehr im Spiel und sind daher als Optimierung weniger effektiv. Das faule Initialisierungsinhaber-Idiom bietet die gleichen Vorteile und ist verständlicher.

Listing 16.6. Faule Initialisierungsinhaber-Klassen-Idiom.

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

So macht man das.

0
Adam

Es gibt immer noch Fälle, in denen eine doppelte Überprüfung durchgeführt werden kann. 

  1. Erstens, wenn Sie wirklich kein Singleton benötigen, und eine doppelte Überprüfung nur für das KEINE Erstellen und Initialisieren vieler Objekte verwendet wird.
  2. Am Ende des Konstruktors/initialisierten Blocks befindet sich ein final-Feld (das bewirkt, dass alle zuvor initialisierten Felder von anderen Threads angezeigt werden).
0
user590444

Das DCL-Problem ist defekt, obwohl es auf vielen VMs zu funktionieren scheint. Es gibt eine nette Berichterstattung über das Problem hier http://www.javaworld.com/article/2075306/Java-concurrency/can-double-checked-locking-be-fixed-.html .

multithreading und Speicher-Kohärenz sind kompliziertere Themen, als sie erscheinen könnten. [...] Sie können diese Komplexität ignorieren, wenn Sie das von Java bereitgestellte Tool für genau diesen Zweck verwenden - die Synchronisierung. Wenn Sie jeden Zugriff auf eine Variable synchronisieren, die möglicherweise von einem anderen Thread geschrieben wurde oder von einem anderen Thread gelesen werden konnte, treten keine Probleme mit der Speicherkohärenz auf.

Die einzige Möglichkeit, dieses Problem richtig zu lösen, besteht darin, eine verzögerte Initialisierung zu vermeiden (tun Sie es eifrig) oder eine einzelne Prüfung in einem synchronisierten Block. Die Verwendung der booleschen Variable initialized entspricht einer Nullprüfung der Referenz selbst. Ein zweiter Thread kann sehen, dass initialized wahr ist, aber instance kann immer noch null oder teilweise initialisiert sein. 

0
Adam

Erstens können Sie für Singletons ein Enum verwenden, wie in dieser Frage erläutert. Implementieren von Singleton mit einem Enum (in Java)

Zweitens können Sie seit Java 1.5 eine flüchtige Variable mit doppelt geprüftem Sperren verwenden, wie am Ende dieses Artikels erläutert: https://www.cs.umd.edu/~pugh/Java/memoryModel/DoubleCheckedLocking.html

0
Victor Ionescu

Ich habe nach dem doppelt geprüften Verriegelungs-Idiom gesucht und aus dem, was ich verstanden habe, könnte Ihr Code zu dem Problem führen, dass eine teilweise konstruierte Instanz gelesen wird, WENN NICHT, dass Ihre Test-Klasse unveränderlich ist:

Das Java-Speichermodell bietet eine besondere Garantie für die Initialisierungssicherheit für das Teilen unveränderlicher Objekte.

Sie können sicher auf sie zugreifen, auch wenn die Synchronisierung nicht zum Veröffentlichen der Objektreferenz verwendet wird.

(Zitate aus dem sehr empfehlenswerten Buch Java Concurrency in Practice)

In diesem Fall würde also die doppelt geprüfte Verriegelungssprache funktionieren.

Wenn dies nicht der Fall ist, beachten Sie, dass Sie die Variableninstanz ohne Synchronisation zurückgeben. Die Instanzvariable kann daher nicht vollständig erstellt werden (Sie würden statt der im Konstruktor angegebenen Werte die Standardwerte der Attribute sehen). 

Die boolesche Variable fügt nichts hinzu, um das Problem zu vermeiden, da sie vor der Initialisierung der Test-Klasse auf "true" gesetzt werden kann (das synchronisierte Schlüsselwort verhindert nicht die Neuordnung der Reihenfolge, einige Regeln ändern möglicherweise die Reihenfolge). Es gibt keine Regel im Java-Speichermodell, um dies zu garantieren.

Das Boolesche Volatilisieren würde auch nichts hinzufügen, da 32-Bit-Variablen in Java atomar erstellt werden. Das doppelt geprüfte Verriegelungswort würde auch mit ihnen funktionieren.

Seit Java 5 können Sie dieses Problem beheben, indem Sie die Instanzvariable als flüchtig deklarieren.

Mehr über das doppelt geprüfte Idiom erfahren Sie in diesem sehr interessanten Artikel .

Zum Schluss noch einige Empfehlungen, die ich gelesen habe:

  • Überlegen Sie, ob Sie das Singleton-Muster verwenden sollten. Es wird von vielen Menschen als Anti-Muster betrachtet. Abhängigkeitsinjektion wird nach Möglichkeit bevorzugt. Check this .

  • Überlegen Sie sorgfältig, ob die doppelte überprüfte Verriegelungsoptimierung vor der Implementierung wirklich notwendig ist, da dies in den meisten Fällen den Aufwand nicht wert wäre. Erwägen Sie auch das Erstellen der Test-Klasse im statischen Feld, da verzögertes Laden nur nützlich ist, wenn eine Klasse erstellt wird, die viele Ressourcen erfordert. In den meisten Fällen ist dies jedoch nicht der Fall.

Wenn Sie diese Optimierung noch durchführen müssen, aktivieren Sie das Kontrollkästchen link , das einige Alternativen enthält, um einen ähnlichen Effekt zu erzielen wie Sie es versuchen.

0
Jorge

Wenn "initialized" wahr ist, MUSS "instance" vollständig initialisiert werden, ebenso wie 1 plus 1 gleich 2 :). Daher ist der Code korrekt. Die Instanz wird nur einmal instanziiert, die Funktion kann jedoch millionenfach aufgerufen werden, wodurch die Leistung verbessert wird, ohne die Synchronisation für eine Million Minuspunkte zu überprüfen.

0
jack

Sie sollten wahrscheinlich die atomaren Datentypen in Java.util.concurrent.atomic verwenden.

0
Seun Osewa