Das Singleton-Muster ist eines der bekanntesten Entwurfsmuster in der objektorientierten Programmierung. Es sorgt dafür, dass von einer Klasse nur genau eine Instanz existiert und bietet einen globalen Zugriffspunkt auf dieses Objekt. In Java kann die Implementierung jedoch schnell problematisch werden, wenn mehrere Threads gleichzeitig auf die Instanz zugreifen – Stichwort Thread-Sicherheit. In diesem Artikel betrachten wir verschiedene Möglichkeiten, wie man ein Singleton in Java korrekt und thread-sicher implementiert.
Warum überhaupt Singleton?
Ein Singleton wird oft eingesetzt, wenn:
- Eine zentrale Steuerinstanz benötigt wird (z. B. Logger, Konfigurationsmanager, Cache).
- Der Zugriff auf gemeinsame Ressourcen zentral geregelt werden soll.
- Eine gemeinsame Datenbasis innerhalb einer Anwendung verfügbar sein soll.
Die Grundform eines Singleton (nicht thread-sicher)
public class Singleton {
private static Singleton instance;
private Singleton() {
// private Konstruktor
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Code-Sprache: PHP (php)
Dieses Beispiel ist nicht thread-sicher. Wenn zwei Threads gleichzeitig getInstance()
aufrufen und instance == null
ist, kann es passieren, dass beide Threads eine neue Instanz erstellen.
Thread-sichere Varianten
1. Synchronisierte Methode (einfach, aber ineffizient)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Code-Sprache: PHP (php)
Vorteile:
- Einfach zu verstehen.
- Thread-sicher.
Nachteile:
- Die Synchronisierung verlangsamt den Zugriff – selbst wenn das Objekt bereits initialisiert ist.
2. Double-Checked Locking (effizienter)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Code-Sprache: PHP (php)
Was ist hier neu?
- Die Variable ist als
volatile
deklariert, um Instruktionsreordering zu verhindern. - Zwei Prüfungen auf
instance == null
, eine außerhalb und eine innerhalb dessynchronized
-Blocks – daher der Name „double-checked locking“.
Vorteile:
- Schnell, sobald die Instanz einmal erzeugt wurde.
- Thread-sicher ohne unnötige Synchronisation.
Nachteile:
- Etwas komplexer zu verstehen.
- Funktioniert erst ab Java 1.5 korrekt (wegen
volatile
).
3. Initialisierung-on-Demand-Holder (empfohlene Methode)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
Code-Sprache: PHP (php)
Funktionsweise:
- Die Instanz wird in einer statischen inneren Klasse gehalten.
- Die Klasse
Holder
wird erst geladen, wenngetInstance()
aufgerufen wird. - Die Java Virtual Machine garantiert, dass die Initialisierung von
INSTANCE
thread-sicher ist.
Vorteile:
- Lazy Loading ohne explizite Synchronisierung.
- Sehr effizient.
- Einfache und elegante Lösung.
Nachteile:
- Weniger bekannt; für einige Entwickler schwerer zu durchschauen.
4. Enum-Singleton (besonders robust)
public enum Singleton {
INSTANCE;
public void doSomething() {
// Methode aufrufen
}
}
Code-Sprache: PHP (php)
Was macht diese Lösung besonders?
- Enums sind per Definition thread-sicher und vor Serialisierungsangriffen geschützt.
- Auch bei Deserialisierung oder Reflection bleibt das Singleton intakt.
Vorteile:
- Kürzester Code.
- Automatisch thread-sicher.
- Schutz gegen komplexe Fehlerquellen (z. B. durch Deserialisierung).
Nachteile:
- Weniger flexibel (z. B. keine Vererbung möglich).
- Für viele Entwickler ungewohnt.
Was ist mit Serialisierung?
Ein Problem klassischer Singleton-Implementierungen ist, dass durch Deserialisierung eine neue Instanz erzeugt werden kann. Um das zu verhindern, sollte man readResolve()
überschreiben:
private Object readResolve() {
return getInstance();
}
Code-Sprache: JavaScript (javascript)
Beim Enum-Singleton tritt dieses Problem nicht auf, da Enums automatisch serialisierungsfest sind.
Zusammenfassung und Empfehlung
Variante | Thread-sicher | Lazy Loading | Einfachheit | Performance |
---|---|---|---|---|
Unsynchronisiert | ❌ | ✅ | ✅ | ✅ |
Synchronisierte Methode | ✅ | ✅ | ✅ | ❌ |
Double-Checked Locking | ✅ | ✅ | ➖ | ✅ |
Static Holder | ✅ | ✅ | ✅ | ✅ |
Enum | ✅ | ❌ (sofort bei Laden) | ✅ | ✅ |
Empfehlung:
- Für die meisten Anwendungen ist die Holder-Variante die beste Wahl.
- Wenn Robustheit gegen Reflection und Serialisierung wichtig ist, sollte man die Enum-Variante verwenden.
- Die synchronisierte Methode ist nur für einfache, nicht performance-kritische Anwendungen geeignet.
Fazit
Die Implementierung eines Singleton in Java scheint auf den ersten Blick trivial – doch gerade in mehrthreadigen Anwendungen kann sie zu unerwarteten Fehlern führen. Java bietet mehrere Möglichkeiten, ein Singleton thread-sicher zu gestalten, jede mit ihren eigenen Vor- und Nachteilen. Wer Wert auf Effizienz und Sicherheit legt, sollte entweder das Initialisierung-on-Demand Holder Pattern oder die Enum-Variante verwenden.