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.

Thread sicherer Container

Bearbeiten

Im folgenden Beispiel wird ein Element einem Container hinzugefügt oder entfernt. Es wird ein Mutex verwendet damit die Funktionen von mehreren Threads nebenläufig ausgeführt werden können.

class Thread_sicherer_Container
{
	container;
	mutex;

	hinzufügen(element)
	{
		mutex.sperren()
		container.hinzufügen(element)
		mutex.entsperren()
	}

	entfernen()
	{
		element
		mutex.sperren()
		if(!container.ist_leer())
		{
			element = container.entfernen()
		}
		mutex.entsperren()
		return element
	}
}

Der Mutex muss in jedem durchgang von entfernen() gesperrt werden. Um das zu umgehen kann eine doppelt überprüfte Sperrung verwendet werden:

class Thread_sicherer_Container
{
	container;
	mutex;
	leer;

	hinzufügen(element)
	{
		mutex.sperren()
		container.hinzufügen(element)
		leer.synchronisiertes_speichern(false)
		mutex.entsperren()
	}

	entfernen()
	{
		element
		if(!leer.synchronisiertes_laden()) // erste Prüfung
		{
			mutex.sperren()
			if(!leer) // zweite Prüfung
			{
				element = container.entfernen()
				leer.synchronisiertes_speichern(container.ist_leer())
			}
			mutex.entsperren()
		}
		return element
	}
}

Falls der Container leer ist, muss nur eine Variable synchronisiert gelesen werden. Dies ist schneller als ein Mutex zu sperren. Die erste Prüfung muss synchronisiert sein, da der Computer ansonsten annehmen kann, dass kein anderer Thread leer verändert und er einen teilweise geschriebener Wert lädt. Bei der zweiten Prüfung stellt der Mutex die Synchronisation sicher.

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 beim ersten Zugriff 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 Objekt zurück.

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#.

class Singleton
{
    private Singleton() { }
    private static 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 readonly 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 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 readonly Singleton 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]

Bearbeiten

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. 2014, ISBN 978-0-9905834-3-1 (englisch).