Hallo Spaß-Coder.
In unserem letzten Artikel zur Serie über Entwurfsmuster haben wir uns mit der Erstellung von Produktfamilien beschäftigt. In diesem Artikel werden wir uns nun um genau ein einziges Exemplar kümmern. Es ist handelt sich um das sog. Einzelstück (singleton).
Wie die Abstract Factory dient auch das Singleton dazu, ein Objekt zu erstellen, mit dem wir dann weiterarbeiten können. Aber warum wollen wir das? Hier gelten die gleichen Gründe, die wir auch schon bei der Abstrakten Fabrik angeführt haben. Wir wollen möglichst auf new verzichten, wenn wir uns in Teilen des Codes bewegen, die sich mit hoher Wahrscheinlichkeit ändern werden.
Beim Singleton geht es darum, genau ein Objekt für unsere gesamte Anwendung zu erstellen. Egal an welcher Stelle wir uns das Singleton holen, es ist immer genau dasselbe. Wenn wir also an einer Stelle der Anwendung einen Status des Sinlgeton-Objekts setzen, hat es an anderer Stelle noch genau denselben Status.
Wann aber ist dieses Entwurfsmuster sinnvoll anzuwenden?
Das gleiche ist noch lange nicht dasselbe
Das Singleton findet Verwendung, wenn wir in unser Anwendung zentral auf das gleiche Objekt zugreifen möchten. Beispiele dafür sind etwa zentrale Konfigurationen oder etwa eine Protokollausgabe, bei der wir von allen Stellen der Anwendung in dasselbe Protokoll schreiben wollen.
Vereinfacht gesagt, immer dann, wenn nur ein Objekt zu einer Klasse existieren darf und ein einfacher Zugriff auf dieses Objekt benötigt wird, ist die Verwendung des Singleton-Musters sinnvoll.
Beispiel
Schauen wir uns als Beispiel einmal an, wie wir einen zentralen Logger implementieren würden. Dazu erstellen wir eine Klasse Logger:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Logger { private static Logger instance; private Logger() { } public static Logger getLogger() { if (instance == null) { instance = new Logger(); } return instance; } public void Log(Object textToLog) { System.out.println(textToLog); } } |
Der wichtigste Aspekt dieser Klasse ist, dass sie einen privaten Konstruktor enthält. Damit stellen wir sicher, das niemand eine Instanz dieser Klasse erstellen kann. Naja, wenigstens fast niemand. Eine Instanz brauchen wir schließlich. Und wer hat Zugriff auf private Klassenmethoden? Genau, nur die Klasse selbst (und ihre inneren Klassen, aber darauf kommen wir bei einem anderen Muster nochmal zurück). Das gilt in gleicher Weise für den Konstruktor.
Nachdem wir nun also sichergestellt haben, dass nur wir eine Instanz unseres Loggers erstellen können, müssen wir diesem aber auch zurückgeben. Dazu enthält die Klasse eine statische Methode getLogger(). Diese Methode dient dazu, die eine Instanz unseres Loggers abzurufen. Wird die Methode das erste Mal aufgerufen, so wird die Instanz erstellt. Von da an wird immer genau diese eine Instanz zurückgegeben.
Dadurch ist immer dann, wenn wir irgendwo in unserem Code eine Protokollausgabe machen möchten sichergestellt, dass wir dieselbe Instanz der Klasse Logger verwenden und somit immer an die gleiche Stelle protokollieren. Gut, oder?
Probleme des Singletons
Leider funktioniert unser obiger Code nicht gut. In kleinen, überschaubaren Anwendungen, die nur in einem Thread arbeiten, ist unser Logger hinreichend gut. Versuchen wir aber nun mit zwei Threads gleichzeitig die Methode getLogger() aufzurufen, bekommen wir ein Problem.
Schaut euch den o.g. Code noch einmal an und geht im Kopf mal durch, was passiert, wenn zwei Threads gleichzeitig die Methode aufrufen. Erkennt ihr das Problem?
Wenn zwei Threads gleichzeitig in die Methode eintreten, ist die Variable instance für beide Threads null, es wird also jeweils eine Neue Instanz erstellt und zurückgegeben. Diese sind dann natürlich unterschiedlich. Wie können wir das Problem lösen?
In Java hilft uns hier das Schlüsselwort synchronized an der Methode:
1 2 3 4 5 6 |
public synchronized static Logger getLogger() { if (instance == null) { instance = new Logger(); } return instance; } |
In C# unterstützt uns das Schlüsselwort lock, es ist aber ein wenig komplizierter zu verwenden. Hier benötigen wir noch ein Objekt, auf welchem wie die Sperre ausführen können:
1 2 3 4 5 6 7 8 9 10 |
private static Object locked = new Object(); public static Logger getLogger() { lock(locked) { if (instance == null) { instance = new Logger(); } return instance; } } |
Dadurch ist sichergestellt, dass auch wirklich nur eine Instanz erstellt wird, auch wenn aus unterschiedlichen Prozessen (Threads) heraus die Abfrage erfolgt.
Alles wird gut!
Damit ist alles gut, oder? Leider nicht. Wir haben zwar eine Lösung für das sog. Nebenläufigkeitsproblem gefunden, das kaufen wir aber teuer ein. Die Synchronisation von Code zwischen verschiedenen Prozessen ist teuer in der Laufzeit. Verwenden wir unser Singleton in unserer Anwendung häufig – und bei unserem Logger-Beispiel ist das nicht soo abwegig – hat das einen erheblichen schlechten Einfluss auf die Laufzeit unserer Anwendung.
Wie aber wollen wir das Problem dann lösen? Nun, zunächst ist die Synchronisation tatsächlich der einzige Weg, das Problem der Nebenläufigkeit in den Griff zu bekommen. Wir können aber versuchen, das Problem mit der Performance in den Griff zu bekommen.
1.) Nichts tun
Hä? Wie jetzt? Seit wann lösen wir Probleme durch aussitzen? Naja, ganz so ist es nicht. Wenn wir unsere Methode getLogger() nur selten und nur in Performance-unkritischen Situationen verwenden, lassen wir den Code genau so wie er ist. Alles weitere ist unnötige Mühe, die wir uns hier machen.
2.) Instanz immer initialisieren
Wir können dafür sorgen, dass unsere Instanz niemals null ist. Dazu müssen wir sie einfach beim Erstellen der Klasse instanziieren:
1 2 3 4 5 |
public class Logger { private static Logger instance = new Logger(); [...] } |
Da die Variable statisch ist, wird sie unmittelbar nach dem Laden der Klasse initialisiert. Ist also in unserem Beispiel immer mit einem Logger gefüllt. Das löst das oben beschriebene Problem.
Es könnte aber wieder zu einem neuen Problem führen. Nämlich genau dann, wenn die Erstellung der Instanz sehr teuer ist, wir diese aber nicht immer in unserer Anwendung benötigen. Denken wir z.B. an eine Schnittstelle zu einem anderen System. Wird einer der Benutzer diese Schnittstelle anfragen? Lohnt es sich, die teure Initialisierung der Schnittstelle durchzuführen, wenn wir das nicht wissen?
Die Antwort ist natürlich „Nein“. Wir sollten in unserem Code nichts machen, was wir nicht auch brauchen.
3.) Teilsynchronisation
Kommt für uns weder die erste, noch die zweite Lösung infrage, müssen wir uns ein wenig Mühe geben, die Synchronisation so performant wie möglich zu halten. Dazu synchronisieren wir nicht die ganze Methode, sondern lediglich die Erstellung unserer Instanz.
Das Ganze sieht in Java dann wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Logger { private volatile static Logger instance; private Logger() { } public static Logger getLogger() { if (instance == null) { synchronized(Logger.class) { if (instance == null) { instance = new Logger(); } } } return instance; } public void Log(Object textToLog) { System.out.println(textToLog); } } |
Wir prüfen also zunächst ohne Synchronisation, ob unsere Instanz schon gefüllt ist. Falls nicht, und nur in diesem Fall und nur genau einmal, führen wir die Erstellung der Instanz synchronisiert durch. Damit die Abfrage auf die Variable aber auch threadübergreifend funktioniert, ist es erforderlich, dass unsere Variable instance mit dem Schlüsselwort volatile versehen wird. Das sorgt dafür, dass die Variable vor jedem Zugriff auf Aktualität geprüft und ggf. aktualisiert wird. Dieser Vorgang ist wesentlich günstiger, als synchronize.
In C# haben wir es nun ein wenig einfacher, da wir die Variable ja bereits definiert haben und wir ohnehin Blöcke und nicht Methoden mit dem Schlüsselwort lock verwenden:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Logger { private static Logger instance; private static Object locked = new Object(); private Logger() { } public static Logger getLogger() { if (instance == null) { lock(locked) { if (instance == null) { instance = new Logger(); } } } return instance; } public void Log(Object textToLog) { System.out.println(textToLog); } } |
Die Funktionsweise ist hier identisch, nur brauchen wir kein weiteres Schlüsselwort für die Variable, da wir hier mit einem separaten Objekt arbeiten. Die Sprachkonzepte zwischen Java und C# sind ein wenig unterschiedlich, die Idee dahinter bleibt aber dieselbe.
Wichtig ist es bei der Suche nach der besten Lösung, sich zu vergegenwärtigen, wie oft ich das Singleton benötige und wie aufwendig die Erstellung ist. Die Lösung ist dementsprechend der obigen Möglichkeiten zu wählen.
Warum muss immer ich das machen??
Wenn ein Framework eingesetzt wird, welches die Verwaltung von Singletons unterstützt – z.B. das SpringFramework – sollte auf die eigene Implementierung verzichtet werden. Insbesondere bei komplexen Anwendungen mit Multithreading ist es – wie oben beschrieben – wichtig, dass das Singleton sauber implementiert ist. Wenn uns ein Framework diese Arbeit abnimmt, sollten wir dies auch nutzen.
Zusammenfassung
Das Singleton ist ein wichtiges uns sehr hilfreiches Muster. Es hat seine Tücken, aber wenn man diese kennt, sind sich auch kein größerer Schrecken mehr als Knecht Ruprecht, nachdem man erfahren hat, dass er in Wirklichkeit gar nicht existiert.
Wir hoffen, dass die Beispiele euch genügend Anhaltspunkte dafür gegeben haben, wann und insbesondere wie das Muster sinnvoll eingesetzt werden kann.
Viel Spaß beim Ausprobieren
Eure Spaß-Coder
Den Code zu unserem Beispiel könnt ihr auf Github finden:
- https://github.com/invidit/CodeQuality/tree/master/DesignPattern/Singleton
Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus:
- https://de.wikipedia.org/wiki/
- Einzelstück (singleton)
- Freeman, Eric & Freeman, Elisabeth – Entwurfsmuster von Kopf bis Fuß – O’Reilly, 2006.
- http://www.zdnet.de/39198058/java-datenzugriffe-mit-dem-schluesselwort-i-volatile-i-synchronisieren/