Idee des Patterns
Häufig ist es so, dass man innerhalb eines Programmes sicherstellen will, dass eine Klasse nur genau einmal instantiiert wird. Alle Komponenten der Software greifen auf diese eine Instanz der Klasse gleichzeitig zu und teilen sich das Objekt. Dabei muss eine einfache Zugriffsmethode vorhanden sein.
Use-Cases eines solchen Patterns sind beispielsweise, wenn innerhalb einer Applikation genau eine Datenbankverbindung hergestellt werden soll, die von unterschiedlichen Teilen des Programmes zusammen genutzt wird. Logging wird häufig auch über ein Singletonpattern realisiert, welches den Zugriff auf die Logging-Datei hält und dafür sorgt, dass Logmeldungen serialisiert in diese offene Datei laufen. Andere Verwendungsmöglichkeiten sind zum Beispiel eine Instanz eines Hauptfensters einer Applikation oder eine Instanz einer Liste von Datenobjekten, die die Applikation verwaltet.
Grundlegende Implementierung des Patterns
Als UML2-Klassendiagramm lässt sich das Singleton-Pattern wie folgt darstellen:
Wird nun eine Instanz dieser Klasse benötigt, so ist das Vorgehen wie folgt:
- Der aufrufende Code ruft die statische Methode getInstance() auf. Für den Aufruf einer statischen Methode muss von der Klasse kein Objekt existieren.
- In der Methode getInstance() befindet sich eine Prüfung, ob das Objekt schon existiert. Wenn nicht, so wird der private Konstruktor aufgerufen und in der lokalen privaten Variable instance hinterlegt.
- Dann wird die lokale private Variable instance zurückgegeben.
Der Code der Methode getInstance() könnte dabei wie folgt aussehen:
1 2 3 4 |
if (instance == null) { instance = new Singleton(); } return instance; |
Dabei ist die Variable instance privat. Dies sichert diese Variable, so dass nicht zu irgendeinem Zeitpunkt innerhalb des Programmes wegen eines Programmierfehlers diese Variable direkt ausgelesen werden kann. Auch der Konstruktor ist privat. Dies bedeutet, dass er nur aus der Klasse selbst aufgerufen werden kann, wie es nämlich hier in der getInstance()-Methode geschieht.
Hier der Code des gesamten Singleton-Patterns:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Singleton { // Variable, die die einzige Instanz der Klasse hält. // Sie ist zugriffsgeschützt von außen und statisch. private static Singleton instance; // Privater Konstruktor. Ebeneso zugriffsgeschützt // von außen. private Singleton() {} // Einzige öffentliche Methode, die vom Rest des // Codes aufgerufen wird public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } |
Betrachten wir nun den Code der getInstance()-Methode nochmals. Da immer geprüft wird, ob die instance-Variable belegt ist und nur, wenn sie nicht belegt ist, ein neues Singleton-Objekt angelegt wird und sie mit diesem belegt wird, kann kein zweites Singleton-Objekt angelegt werden. Die if-Bedingung kann auf der anderen Seite nur genau einmal wahr sein und in den if-Block springen, nämlich genau beim ersten Mal des Aufrufes der getInstance()-Methode. Wir haben also eine Möglichkeit erzeugt, nur ein Objekt dieser Klasse innerhalb des gesamten Programmes zuzulassen und damit die Singleton-Idee implementiert.
Multi-Threading
Wir haben gerade gesagt, dass unsere Implementierung des Singleton-Patterns unser System dazu zwingt, dass genau nur ein Objekt zur Laufzeit vorhanden ist. Dies ist auch bei einer Single-Thread-Umgebung, wo es nur einen Programmfaden gibt, der Fall. Arbeiten jedoch mehrere Threads im Programm, so kann es sein, dass der Thread in der getInstance()-Methode an Position (*) gestoppt wird:
1 2 3 4 5 |
if (instance == null) { // (*) instance = new Singleton(); } return instance; |
Ruft nun ein zweiter Thread die getInstance()-Methode auf, so ist die Variable instance weiterhin nicht belegt. Eine neue Instanz kann angelegt werden und diese der Variable zugewiesen werden. Die Instanz wird von der Methode in das Programm übergeben. Wenn nun jedoch wieder der erste Thread wieder Rechenzeit erhält, d.h. weiterläuft, so prüft dieser nicht mehr ob die Variable noch nicht belegt ist und der Konstruktor wird ein zweites Mal aufgerufen und die Variable instance überschrieben und diese zweite Instanz zurückgegeben. Innerhalb unseres Systems existieren nun genau zwei Instanzen des Singletons, was vermieden werden muss.
In dem folgenden Video ist das Problem an einem Live-Beispiel demonstriert:
Dieses Problem kann gelöst werden, indem man die Java Virtual Machine anweist, dass nur ein Thread diese Methode gleichzeitig betreten darf. Dies geschieht mit dem Keyword synchronized:
1 2 3 |
public static synchronized Singleton getInstance() { ... } |
Ein Problem bei dieser Lösung ist es nun jedoch, dass eine synchronized-Methode für jede Anfrage des Singleton-Objektes aufgerufen werden muss. Auch bei schon bestehendem Singleton-Objekt müssen Threads hintereinander gereiht werden. Zusätzlich hat die Java Virtual Machine Verwaltungsaufwand für diese synchronized-Methode. Aufgrund von beiden Tatsachen wird die Applikation langsam. Dabei ist dies gar nicht notwendig. Die Methode sollte nur dann synchronized sein, wenn die Instanz noch nicht erzeugt worden ist, d.h. der erste Aufruf der Methode noch nicht abgeschlossen ist.
Einen Ausweg aus diesem Performance-Problem bietet die statische Instantiierung des Singleton-Objektes. Dies tut der folgende Programmcode:
1 2 3 4 5 6 7 |
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } |
Hier wird der Konstruktor der Singletonklasse schon zur Ladezeit durch den ClassLoader aufgerufen, der eine threadsichere Umgebung schafft. Erhält dann schließlich das Programm diese Klasse, so ist die instance-Variable schon vorbelegt. Wir müssen keine weitere Instantiierung durchführen.
Dieser Ansatz wird als der zu verfolgende Ansatz von der Oracle-Java-Entwicklungsgruppe bezeichnet. Mit Hilfe dieser einfachen Implementierung des Singleton-Patterns ist der Entwickler auf der sicheren ,,thread-safe“ Seite. Hingehen könnte es sein, dass das Programm zwar die Singleton-Klasse nutzt, jedoch die getInstance()-Methode niemals aufruft. Da das Singleton-Objekt in der Regel ein schwergewichtiges Objekt ist, gehen wertvolle Ressourcen dadurch verloren. Man nennt das Vorgehen dieser Implementierung mit der statischen Initialisierung ein Eager-Verhalten. Wir hätten jedoch gerne wieder ein Lazy-Verhalten, wie die Lösung mit der synchronized-Methode.
An dieser Stelle muss darauf aufmerksam gemacht werden, dass eine Singleton-Klasse, die noch andere Funktionalitäten hat, einen Bad Smell im Code darstellt, also ein Klassendesignproblem aufzeigt. Die Funktionalitäten sollten in zwei unterschiedliche Klassen ausgelagert werden, so dass das Singleton nicht sinnlos instantiiert werden muss.
Ist man aus irgendwelchen Gründen doch gezwungen, die Klasse so zu belassen, wie sie ist, so kann man trotzdem das Lazy-Verhalten implementieren, indem man Double-Checked-Locking (Doppelt-überprüfte Sperrung) verwendet. Der Code sieht dabei wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } } |
Der Unterschied zu der einfachen synchronized-Methode ist, dass hier die Methode nicht mehr synchronized ist, d.h. schnell aufgerufen werden kann. Ist dann die Instanz gesetzt, so wird nie wieder in den Ausführungsblock der äußeren If-Abfrage gesprungen, weshalb der dort beinhaltete synchronized-Block keine Rolle mehr spielt. Das Problem, dass ein Thread schon geprüft hat, ob die Variable noch unbesetzt ist und dann abgebrochen wird und durch einen anderen Thread in dem Moment die Variable belegt wird, wird umgangen, indem innerhalb des dargestellten synchronized-Blocks, die Variable instance nochmals geprüft wird. Zusätzlich muss für dieses Vorgehen die instance-Variable als volatile gekennzeichnet sein. Der CPU-Kern hat die Möglichkeit häufig benutzte Variablen nicht sofort wieder in den Hauptspeicher zu schreiben, sondern in sich zu halten, was einen Geschwindigkeitsvorteil bietet. Mit volatile wird sie jedoch angewiesen, die Variable immer sofort zurück zu schreiben, so dass ein anderer CPU-Kern, der einen anderen Thread bedient nicht eine nicht-aktuelle Speicherkopie der Variable im ungesetzten Zustand sieht.
Diese Double-Checked-Locking-Implementierung wird von vielen als ein Anti-Pattern gesehen. Dies ist zum einen deswegen der Fall, dass wie oben schon gesagt die Verwendung ein Zeichen ist, dass die Klasse zwei unterschiedlichen Zwecken dient. Zum anderen schleichen sich in die komplizierte Implementierung des Double-Checked-Lockings schnell Fehler ein, die mühsam zu finden sind.
Grenzen des Patterns
Dem Pattern sind Grenzen gesetzt, dass die Start-Up-Zeit eines Programmes eventuell sehr lang ist, da viele Singletons instantiiert werden müssen. Weiterhin wird das Singleton-Pattern häufig auch dort benutzt, wo eigentlich eine statische vorinitialisierte ausgereicht hätte. Hier wird der Code durch das Singleton-Pattern unnötig kompliziert.