Double-Checked Locking mit Singleton

1. Einleitung

In diesem Tutorial werden wir über das doppelt überprüfte Verriegelungsmuster sprechen. Dieses Muster reduziert die Anzahl der Sperrenerfassungen, indem einfach zuvor der Sperrzustand überprüft wird. Infolgedessen kommt es normalerweise zu einer Leistungssteigerung.

Schauen wir uns genauer an, wie es funktioniert.

2. Implementierung

Betrachten wir zunächst einen einfachen Singleton mit drakonischer Synchronisation:

public class DraconianSingleton { private static DraconianSingleton instance; public static synchronized DraconianSingleton getInstance() { if (instance == null) { instance = new DraconianSingleton(); } return instance; } // private constructor and other methods ... }

Obwohl diese Klasse threadsicher ist, können wir feststellen, dass es einen klaren Leistungsnachteil gibt: Jedes Mal, wenn wir die Instanz unseres Singletons erhalten möchten, müssen wir eine möglicherweise unnötige Sperre erwerben.

Um dies zu beheben, könnten wir stattdessen zunächst überprüfen, ob wir das Objekt überhaupt erstellen müssen, und nur in diesem Fall würden wir die Sperre erwerben.

Wenn wir weiter gehen, möchten wir dieselbe Prüfung erneut durchführen, sobald wir den synchronisierten Block betreten, um die Operation atomar zu halten:

public class DclSingleton { private static volatile DclSingleton instance; public static DclSingleton getInstance() { if (instance == null) { synchronized (DclSingleton .class) { if (instance == null) { instance = new DclSingleton(); } } } return instance; } // private constructor and other methods... }

Eine Sache, die Sie bei diesem Muster beachten sollten, ist, dass das Feld volatil sein muss , um Probleme mit der Cache-Inkohärenz zu vermeiden. Tatsächlich ermöglicht das Java-Speichermodell die Veröffentlichung teilweise initialisierter Objekte, was wiederum zu subtilen Fehlern führen kann.

3. Alternativen

Obwohl die doppelt überprüfte Verriegelung möglicherweise die Dinge beschleunigen kann, gibt es mindestens zwei Probleme:

  • Da das flüchtige Schlüsselwort ordnungsgemäß funktionieren muss, ist es nicht mit Java 1.4 und niedrigeren Versionen kompatibel
  • Es ist ziemlich ausführlich und macht es schwierig, den Code zu lesen

Lassen Sie uns aus diesen Gründen einige andere Optionen ohne diese Mängel untersuchen. Alle folgenden Methoden delegieren die Synchronisierungsaufgabe an die JVM.

3.1. Frühe Initialisierung

Der einfachste Weg, um Thread-Sicherheit zu erreichen, besteht darin, die Objekterstellung zu integrieren oder einen äquivalenten statischen Block zu verwenden. Dies nutzt die Tatsache aus, dass statische Felder und Blöcke nacheinander initialisiert werden (Java Language Specification 12.4.2):

public class EarlyInitSingleton { private static final EarlyInitSingleton INSTANCE = new EarlyInitSingleton(); public static EarlyInitSingleton getInstance() { return INSTANCE; } // private constructor and other methods... }

3.2. Initialisierung bei Bedarf

Da wir aus der Referenz zur Java-Sprachspezifikation im vorherigen Absatz wissen, dass eine Klasseninitialisierung bei der ersten Verwendung einer ihrer Methoden oder Felder erfolgt, können wir eine verschachtelte statische Klasse verwenden, um die verzögerte Initialisierung zu implementieren:

public class InitOnDemandSingleton { private static class InstanceHolder { private static final InitOnDemandSingleton INSTANCE = new InitOnDemandSingleton(); } public static InitOnDemandSingleton getInstance() { return InstanceHolder.INSTANCE; } // private constructor and other methods... }

In diesem Fall weist die InstanceHolder- Klasse das Feld beim ersten Zugriff zu, indem wir getInstance aufrufen .

3.3. Enum Singleton

Die letzte Lösung stammt aus dem Effective Java- Buch (Punkt 3) von Joshua Block und verwendet eine Aufzählung anstelle einer Klasse . Zum Zeitpunkt des Schreibens wird dies als die präziseste und sicherste Art angesehen, einen Singleton zu schreiben:

public enum EnumSingleton { INSTANCE; // other methods... }

4. Fazit

Zusammenfassend ging dieser kurze Artikel durch das doppelt überprüfte Sperrmuster, seine Grenzen und einige Alternativen.

In der Praxis machen die übermäßige Ausführlichkeit und die mangelnde Abwärtskompatibilität dieses Muster fehleranfällig, und daher sollten wir es vermeiden. Stattdessen sollten wir eine Alternative verwenden, mit der die JVM die Synchronisierung durchführen kann.

Wie immer ist der Code aller Beispiele auf GitHub verfügbar.