Java - von am Saturday, August 26, 2006 0:19 - 17 Kommentare

Singleton Pattern in Java

Während der Softwareentwicklung ist es manchmal notwendig, sicherzustellen, daß nur eine Instanz eines Objekts existiert. Dieser Fall tritt zum Beispiel auf, wenn Informationen in eine Protokolldatei geschrieben werden sollen. Um kontrollieren zu können, daß nur ein solches Objekt – und nicht mehrere parallel – in diese Datei schreiben, kann das Singleton-Pattern eingesetzt werden. Mit dem Singleton Pattern kann dafür gesorgt werden, daß nur eine Instanz des als Singleton implementierten Objekts existiert.

Allerdings gibt es hier noch eine Kleinigkeit zu beachten, die nicht jedem Entwickler bekannt ist: es existiert immer nur eine Instanz pro ClassLoader! In einer MultiThreading-Umgebung mit mehreren ClassLoadern, wie sie in ServletContainern üblich sind, ist es nicht unwahrscheinlich, daß das Objekt, welches als Singleton implementiert ist, mehrfach existiert. Wir werden die verschiedenen Implementierungen des Singleton Patterns durchgehen, und diese schrittweise zu einer möglichen Idealimplementierung ausbauen.

Zum leichteren Verständnis der Design Patterns möchte ich Ihnen den Bestseller Entwurfsmuster von Kopf bis Fuß nahelegen, welches die meiner Meinung nach verständlichste und beste Einführung in die Welt der Design Patterns bietet.

Kategorie

Das Singleton Pattern wird zu den “Creational Patterns” gezählt, da es Objekte erzeugt und zurückliefert.

Einsatzbereich

Das Singleton Pattern kann eingesetzt werden, wenn:

  • … sichergestellt werden soll, daß das betreffende Objekt nur einmal instanziiert wird
  • … ein globaler Zugriffspunkt auf das Objekt existieren soll
  • … die Instanziierung des Objekts es ermöglichen soll, ggf. in der Zukunft doch mehrere Instanzen zu instanziieren, ohne die Clients reimplementieren zu müssen

UML-Klassendiagramm: Singleton Pattern

Das UML-Klassendiagramm für das Singleton Pattern sieht folgendermaßen aus:

UML-Modell: Singleton Pattern

Das klassische Singleton

Die klassische Implementation eines Singleton verhindert die direkte Instanziierung des Objekts über den Standardkonstuktor, indem Sie für diesen den modifier private verwendet. Damit jetzt aber auch außerhalb der Klasse eine Instanz angefordert werden kann, hat es sich “eingebürgert”, eine statische Methode namens getInstance() bereitzustellen, die die einzige Instanz der Klasse zurückliefert. Diese einzige Instanz wiederum wird als statisches Attribut der Klasse gespeichert, die auch per private-modifier vor dem Zugriff außerhalb der Klasse geschützt wird. Die Implementation sieht damit folgendermaßen aus:

  1. package de.theserverside.designpatterns.singleton;
  2.  
  3. /**
  4.  * Klassische Implementierung des Singleton-Patterns
  5.  */
  6. public class ClassicSingleton {
  7.     private static ClassicSingleton instance = null;
  8.  
  9.     /**
  10.      * Default-Konstruktor, der nicht außerhalb dieser Klasse
  11.      * aufgerufen werden kann
  12.      */
  13.     private ClassicSingleton() {}
  14.  
  15.     /**
  16.      * Statische Methode, liefert die einzige Instanz dieser
  17.      * Klasse zurück
  18.      */
  19.     public static ClassicSingleton getInstance() {
  20.         if (instance == null) {
  21.             instance = new ClassicSingleton();
  22.         }
  23.         return instance;
  24.     }
  25. }

Der obige Code implementiert das Konzept der lazy initialization, d.h. das Objekt wird erst instanziiert, wenn es erstmalig angefordert wird. Der Vorteil ist, daß, wenn das Singleton niemals verwendet wird, getInstance() also niemals aufgerufen wird, auch kein Speicher (und damit ggf. verbunde Resourcen) belegt wird.

Bei dieser Implementierung gibt es allerdings ein Problem: in single-thread Anwendungen funktioniert sie wunderbar, in Multithreadingumgebungen kann sie aber scheitern! Wenn von zwei Threads der erste getInstance() aufruft, die Zuweisung instance = new ClassicSingleton() noch nicht erreicht wurde, parallel der zweite Thread in diese Methode tritt und die Bedingung if (instance == null) { erfüllt wird, dann werden zwei verschiedene Instanzen erzeugt. Damit wäre das Singleton-Konzept ausgehebelt und die Klasse nutzlos.

Synchronisierter Methodenzugriff

“Moment mal!”, werden Sie sagen, “Wieso synchronisieren wir nicht einfach den Zugriff auf die Methode?”. Nun gut, schauen wir uns einmal an, wie man das implementieren könnte:

  1. package de.theserverside.designpatterns.singleton;
  2.  
  3. /**
  4.  * Implementierung des Singleton-Patterns mit synchronisiertem
  5.  * Methodenzugriff
  6.  */
  7. public class SynchronizedSingleton {
  8.     private static SynchronizedSingleton instance = null;
  9.  
  10.     /**
  11.      * Default-Konstruktor, der nicht außerhalb dieser Klasse
  12.      * aufgerufen werden kann
  13.      */
  14.     private SynchronizedSingleton() {}
  15.  
  16.     /**
  17.      * Statische Methode, liefert die einzige Instanz dieser
  18.      * Klasse zurück
  19.      */
  20.     public static synchronized SynchronizedSingleton getInstance() {
  21.         if (instance == null) {
  22.             instance = new SynchronizedSingleton();
  23.         }
  24.         return instance;
  25.     }
  26. }

Ja, dieser Code ist tatsächlich thread-safe… das Problem ist allerdings, daß nur der erste Aufruf dieser Methode synchronized sein muss, da fortlaufende Aufrufe dann direkt ein bereits instanziiertes Singleton zurückgeliefert bekommen. Methoden, die mit dem modifier synchronized versehen sind, können auf älteren JVMs bis zu 100x langsamer laufen! In einer Singleton-Implementierung, die z.B. ständig Informationen in eine Datei loggt (und aus welchen Gründen auch immer das performante log4j-Paket nicht verwendet) kann dies die Geschwindigkeit der Anwendung drastisch reduzieren. Wir müssen uns also nach einer anderen Lösung umschauen…

Checked locking

Wie wäre es, wenn wir nur den kritischen Teil der Methode synchronisieren, also nur die Zeile mit der Instanziierung des Objekts samt Zuweisung an das Klassenattribut instance? Damit würde doch eigentlich nur der erste Zugriff auf die Methode in einen synchronized-Block laufen – und nicht die gesamte Methode bei jedem Aufruf – oder?

  1. package de.theserverside.designpatterns.singleton;
  2.  
  3. /**
  4.  * Implementierung des Singleton-Patterns mit synchronisiertem
  5.  * Zugriff auf die Instanziierung und einfacher Bedingungsprüfung
  6.  */
  7. public class CheckedLockingSingleton {
  8.     private static CheckedLockingSingleton instance = null;
  9.  
  10.     /**
  11.      * Default-Konstruktor, der nicht außerhalb dieser Klasse
  12.      * aufgerufen werden kann
  13.      */
  14.     private CheckedLockingSingleton() {}
  15.  
  16.     /**
  17.      * Statische Methode, liefert die einzige Instanz dieser
  18.      * Klasse zurück
  19.      */
  20.     public static CheckedLockingSingleton getInstance() {
  21.         if (instance == null) {
  22.             synchronized(CheckedLockingSingleton.class) {
  23.                 instance = new CheckedLockingSingleton();
  24.             }
  25.         }
  26.         return instance;
  27.     }
  28. }

Auf den ersten Blick sieht es so aus, als würde das funktionieren. Aber jetzt stellen Sie sich das folgende Szenario vor: der erste Thread gelangt in den synchronized-Block und erhält seinen Lock. Bevor allerdings das Singleton instanziiert und zugewiesen werden kann, übernimmt Thread 2 die Kontrolle und überwindet die if-Bedingung. Thread 2 steht damit direkt vor dem synchronized-Block und muss warten, bis Thread 1 den Lock freigibt. Thread 1 übernimmt nun wieder die Kontrolle und instanziiert das Singleton, weist es dem Klassenattribut instance zu und liefert es per return instance an die aufrufende Methode zurück. Wenn jetzt Thread 2 weiterlaufen darf, wird ein neues Objekt instanziiert und zurückgegeben. Damit ist dieser Code nicht thread-safe und sollte nicht verwendet werden.

Double-checked locking

An dieser Stelle ist ein Idiom entstanden, das als double-checked locking bezeichnet wird, welches den Zugriff auf die Instanziierung mittels einer zweiten Bedingung schützen soll. Das soll sicherstellen, daß die Instanziierung nur durchgeführt wird, wenn wirklich noch keine Instanz angelegt wurde. Die Implementierung sieht hier folgendermaßen aus:

  1. package de.theserverside.designpatterns.singleton;
  2.  
  3. /**
  4.  * Implementierung des Singleton-Patterns mit synchronisiertem
  5.  * Zugriff auf die Instanziierung und zweifacher Bedingungsprüfung
  6.  */
  7. public class DoubleCheckedLockingSingleton {
  8.     private static DoubleCheckedLockingSingleton instance = null;
  9.  
  10.     /**
  11.      * Default-Konstruktor, der nicht außerhalb dieser Klasse
  12.      * aufgerufen werden kann
  13.      */
  14.     private DoubleCheckedLockingSingleton() {}
  15.  
  16.     /**
  17.      * Statische Methode, liefert die einzige Instanz dieser
  18.      * Klasse zurück
  19.      */
  20.     public static DoubleCheckedLockingSingleton getInstance() {
  21.         if (instance == null) {
  22.             synchronized(DoubleCheckedLockingSingleton.class) {
  23.                 if (instance == null) {
  24.                     instance = new DoubleCheckedLockingSingleton();
  25.                 }
  26.             }
  27.         }
  28.         return instance;
  29.     }
  30. }

Das könnte doch funktionieren… wäre da nicht das Speichermodell der Java-Plattform! Die zweite Bedingung if (instance == null) in Zeile 23 kann nämlich zu true evaluieren, ohne daß new DoubleCheckedLockingSingleton() aufgerufen, und das instanziierte Objekt dem Klassenattribut instance zugewiesen wurde! Um das exemplarisch zu verdeutlichen, schauen wir uns den folgenden Pseudo-Bytecode für den Befehl instance = new DoubleCheckedLockingSingleton() an:

  1. // Speicher alloziieren
  2. ptrMemory = allocateMemory()
  3. // Den Speicher dem Klassenattribut zuweisen, ab hier gilt: instance != null
  4. assignMemory(instance, ptrMemory)
  5. // Den Konstruktor aufrufen, das Objekt ist dann ab hier korrekt instanziiert
  6. callDoubleCheckedLockingSingletonConstructor(instance)

Zwischen der 4. und der 6. Zeile könnte die Ausführung des Threads durch die Java Virtual Machine unterbrochen, und die Ausführung eines zweiten Threads vorgezogen werden. Die zweite Bedingung if (instance == null) würde damit zu true evaluieren, ohne daß bisher der Konstruktor (Zeile 6) aufgerufen worden wäre. Das double-checked locking ist damit nicht sicher.
Konstrukte wie diese sind in JIT-Compilern nicht unüblich und da man nicht immer voraussagen kann, wo der Code läuft sollte man das double-checked locking vermeiden, um auf der sicheren Seite zu stehen.

Eine Lösung muss her…

Nach all den Synchronisierungen, Bedingungen und Zuweisungen gibt es momentan nur eine Implementierung, die alle genannten Probleme löst: das Objekt muss beim erstmaligen Zugriff auf die Klasse durch die JVM instanziiert werden. Das erreichen wir, indem wir den Aufruf des Konstruktors schon bei der Deklaration des Klassenattributs instance durchführen:

  1. package de.theserverside.designpatterns.singleton;
  2.  
  3. /**
  4.  * Performante und thread-safe Implementierung des Singleton-Patterns
  5.  */
  6. public class Singleton {
  7.     private static Singleton instance = new Singleton();
  8.  
  9.     /**
  10.      * Default-Konstruktor, der nicht außerhalb dieser Klasse
  11.      * aufgerufen werden kann
  12.      */
  13.     private Singleton() {}
  14.  
  15.     /**
  16.      * Statische Methode, liefert die einzige Instanz dieser
  17.      * Klasse zurück
  18.      */
  19.     public static Singleton getInstance() {
  20.         return instance;
  21.     }
  22. }

Mit dieser Lösung werden die Performanceprobleme, die mit eine Synchronisation mit sich bringt, eliminiert und es ist gewährleistet, daß das Objekt auch in einer Multithreadingumgebung stets nur einmal instanziiert wird.

Sourcecode zum Artikel

Sourcecode: Singleton Pattern

Weiterführende Literatur

Wer sich mit Design Patterns näher auseinandersetzen will, kommt um das Standardwerk der “Gang of Four” (GoF) “Design Patterns” nicht herum. Der Link führt zur englischen Ausgabe des Buches, da die deutsche Übersetzung sehr grausam ist…

Wenn Ihnen dieses Buch zu trocken ist und Sie leichter verdaubares Material suchen, sind die beiden folgenden Bücher zum Thema “Design Patterns”, bzw. “Design Patterns in Java” auch sehr zu empfehlen.

Eine unterhaltsame und sehr leicht verständliche Einführung in die Thematik Design Patterns in Java finden Sie mit “Entwurfsmuster von Kopf bis Fuß“. Das gesamte Buch ist bebildert und anschaulich gestaltet, und die Konzepte werden didaktisch hervorragend erläutert. Die Codebeispiele sind in Java geschrieben und auf das Nötigste reduziert.

Das Buch “The Design Patterns Java Workbook” arbeitet die Design Patterns der Reihe nach ab, aber nicht, ohne vorher die Grundlagen (Interfaces, Abstrakte Klassen etc.) zu erläutern. Jedes Kapitel enthält Aufgaben, die gelöst werden können und anhand derer das Erlernte sofort praktisch angewandt werden kann. Da bisher keine deutsche Übersetzung existiert (Stand: August 2006), müssen Sie noch mit der englischen Fassung vorlieb nehmen.

Be Sociable, Share!


Kommentare

Kommentieren

Weitere Empfehlungen: