Doppelt überprüfte Sperrung

aus Wikipedia, der freien Enzyklopädie
Wechseln zu: Navigation, Suche

Eine doppelt überprüfte Sperrung (englisch double-checked locking) ist ein Muster in der Softwareentwicklung, welches dazu dient den Zugriff auf ein gemeinsames Objekt durch mehrere gleichzeitig laufende Threads zu regeln.

Eine falsch implementierte doppelt überprüfte Sperrung ist ein Antimuster. Dies passiert oft unerfahrenen Programmierern, die von der Problematik des Lockings wissen, aber die falschen Schlüsse ziehen.

Doppelt überprüfte Sperrung in Java[Bearbeiten]

Obwohl mit Java 5 unter einer neuen Semantik des Schlüsselwortes volatile eine doppelt überprüfte Sperrung threadsicher realisiert werden kann, gilt es immer noch als Anti-Pattern, da es zu umständlich und ineffizient ist. Zudem ist der Effizienznachteil von volatile kaum kleiner als von synchronized.

Beispiel

Das folgende Beispiel zeigt die Problematik in der getHelper()-Methode, in der für jedes Foo-Objekt genau ein Helper-Objekt engl. lazy erzeugt werden soll:

public class Foo {
   private Helper helper = null; 
 
   public Helper getHelper() {
     if(helper == null) // erste Prüfung     synchronized(this) {
       if(helper == null) // zweite Prüfung         helper = new Helper();
     }
     return helper; 
   }
 
   // ...
}

Die Schnittstelle Helper wird genutzt, um außerhalb eines Foo-Objektes auf dem Helper-Objekt arbeiten zu können. Definiert man wie hier helper nicht als volatile, ist die doppelte Prüfung problematisch, weil z. B. ein Java JIT-Compiler den Assemblercode so umsortieren kann, dass der Verweis auf das Helper-Objekt gesetzt wird, bevor der Konstruktor vom Helper-Objekt vollständig durchlaufen wurde. In diesem Fall liefert getHelper() ein nicht initialisiertes Object zurück.

Lösung[Bearbeiten]

Ab Java 5 werden volatile definierte Variablen erst nach vollständiger Abarbeitung des Konstruktors sichtbar. Wird also die Variable helper als volatile definiert, läuft obiges Beispiel korrekt durch.

Falls – wie bei der Implementierung eines Singletons – nur eine einzige Instanz pro Klasse existieren soll, gibt es eine leicht zu implementierende Lösung: Das Attribut wird als static deklariert und die Erzeugung des Objekts in eine Unterklasse (hier: nested class) ausgegliedert – das sog. initialization on demand holder-Idiom.

public class Foo {
   private static class HelperHolder {
      public static Helper helper = new Helper();
   }
 
   public Helper getHelper() {
      return HelperHolder.helper; 
   }
 
   // ...

Hierbei wird das statische Attribut der Klasse HelperHolder erst beim Aufruf durch getHelper() instanziert[1], also „lazy“, und die Virtuelle Maschine sorgt für die Threadsicherheit.[2]

Doppelt überprüfte Sperrung in C#[Bearbeiten]

Analog zu Java existiert die doppelt geprüfte Sperrung auch in C#. Für eine korrekte Implementierung muss hierbei zwingend das volatile-Schlüsselwort eingesetzt werden.

class Singleton 
{
    private Singleton() { }
    private static volatile Singleton instance; 
    public static Singleton Instance 
    {
        get
        {
            if (instance == null)             {                lock(_lock)                {                      if (instance == null)                     {
                        instance = new Singleton();
                    }
                }   
            }
            return instance;
        }
    }
 
    // Hilfsfeld für eine sichere Threadsynchronisierung    private static object _lock = new object();}

Soll kein dezidiertes Hilfsfeld genutzt werden kann, wie in Java, auf this gelockt werden. Dies gilt jedoch als bad-practice[3], da hierdurch leicht sog. Deadlocks entstehen können. Ein dezidiertes Lock-Objekt ist in so gut wie jedem Falle vorzuziehen.

class Singleton 
{
    private Singleton() { }
    private static volatile Singleton instance;
 
    public static Singleton Instance 
    {
        get        {
            if (instance == null) 
            {
                lock (this)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
 
            return instance;
        }
    }
}

Eine weitere Möglichkeit besteht darin, die Singleton-Variable direkt in der Felddeklaration zu initialisieren. Dadurch ist die überprüfte Sperrung überflüssig, jedoch ist die Initialisierung dann nicht mehr lazy, bzw. wird beim ersten Zugriff auf die Klasse durchgeführt. Threadsicherheit wird durch die CLR gewährt.

class Singleton 
{
    private Singleton() { }
    private static ingleton instance = new Singleton();
 
    public static Singleton Instance 
    {
        get        {
            return instance;
        }
    }
}

Die beste Möglichkeit ab .NET 4.0 ist der Einsatz der Lazy<T>-Klasse[4], welche intern eine korrekte Form der doppelt überprüften Sperrung verwendet.[5]

Einzelnachweise[Bearbeiten]

  1. Java Language Specification, Java SE 7 Edition: 12.4.1
  2. Java Language Specification, Java SE 7 Edition: 12.4.2
  3. lock-Statement (C#-Reference). In: MSDN. Microsoft, abgerufen am 17. Oktober 2014 (englisch).
  4. Lazy<T> Class. In: MSDN. Microsoft, abgerufen am 27. Juli 2014 (englisch).
  5.  Ben Watson: Writing High-Performance .NET Code. 23. Juli 2014, ISBN 9780990583431.