Einführung
Das Java Singleton-Muster ist eines der Viererbande-Entwurfsmuster und gehört zur Kategorie der Erzeugungsmuster. Aus der Definition scheint es sich um ein einfaches Entwurfsmuster zu handeln, aber bei der Implementierung treten viele Bedenken auf.
In diesem Artikel werden wir die Grundprinzipien des Singleton-Entwurfsmusters erlernen, verschiedene Möglichkeiten zur Implementierung des Singleton-Entwurfsmusters erkunden und einige bewährte Verfahren für dessen Verwendung besprechen.
Prinzipien des Singleton-Musters
- Das Singleton-Muster beschränkt die Instanziierung einer Klasse und stellt sicher, dass nur eine Instanz der Klasse in der Java Virtual Machine existiert.
- Die Singleton-Klasse muss einen globalen Zugriffspunkt bereitstellen, um die Instanz der Klasse zu erhalten.
- Das Singleton-Muster wird für Protokollierung, Treiberobjekte, Zwischenspeicherung und Thread-Pool verwendet.
- Singleton-Entwurfsmuster wird auch in anderen Entwurfsmustern wie Abstrakte Fabrik, Builder, Prototyp, Fassade, usw. verwendet.
- Singleton-Entwurfsmuster wird auch in Core-Java-Klassen verwendet (zum Beispiel
java.lang.Runtime
,java.awt.Desktop
).
Implementierung des Singleton-Entwurfsmusters in Java
Um das Singleton-Entwurfsmuster zu implementieren, gibt es verschiedene Ansätze, aber alle haben die folgenden gemeinsamen Konzepte.
- Privater Konstruktor, um die Instanziierung der Klasse durch andere Klassen zu beschränken.
- Private statische Variable derselben Klasse, die die einzige Instanz der Klasse ist.
- Öffentliche statische Methode, die die Instanz der Klasse zurückgibt. Dies ist der globale Zugriffspunkt für die äußere Welt, um die Instanz der Singleton-Klasse zu erhalten.
In den weiteren Abschnitten werden verschiedene Ansätze zur Implementierung des Singleton-Entwurfsmusters und Design-Überlegungen mit der Implementierung behandelt.
1. Eifrig initialisieren
Bei der eifrigen Initialisierung wird die Instanz der Singleton-Klasse zum Zeitpunkt des Klassenladens erstellt. Der Nachteil der eifrigen Initialisierung besteht darin, dass die Methode erstellt wird, selbst wenn die Client-Anwendung sie möglicherweise nicht verwendet. Hier ist die Implementierung der Singleton-Klasse mit statischer Initialisierung:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// privater Konstruktor, um die Verwendung durch Client-Anwendungen zu verhindern
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
Wenn Ihre Singleton-Klasse nicht viele Ressourcen verwendet, ist dies der Ansatz, den Sie verwenden sollten. Aber in den meisten Szenarien werden Singleton-Klassen für Ressourcen wie Dateisysteme, Datenbankverbindungen, usw. erstellt. Wir sollten die Instanziierung vermeiden, es sei denn, der Client ruft die getInstance
-Methode auf. Außerdem bietet diese Methode keine Optionen für die Ausnahmebehandlung.
2. Initialisierung durch statischen Block
Die Initialisierung des statischen Blocks erfolgt ähnlich wie die sofortige Initialisierung, mit dem Unterschied, dass die Instanz der Klasse im statischen Block erstellt wird, was die Möglichkeit für Fehlerbehandlung bietet.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// Statische Blockinitialisierung für Fehlerbehandlung
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
Sowohl die sofortige Initialisierung als auch die Initialisierung des statischen Blocks erstellen die Instanz, bevor sie verwendet wird, und das ist keine bewährte Praxis.
3. Träge Initialisierung
Die Methode der trägen Initialisierung zur Implementierung des Singleton-Musters erstellt die Instanz in der globalen Zugriffsmethode. Hier ist der Beispielcode zur Erstellung der Singleton-Klasse mit diesem Ansatz:
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
Die vorherige Implementierung funktioniert gut im Falle der Einzelthread-Umgebung, aber wenn es um Mehrfaden-Systeme geht, kann es Probleme verursachen, wenn mehrere Threads gleichzeitig innerhalb der if
-Bedingung sind. Es zerstört das Singleton-Muster und beide Threads erhalten verschiedene Instanzen der Singleton-Klasse. Im nächsten Abschnitt werden wir verschiedene Möglichkeiten sehen, um eine thread-sichere Singleton-Klasse zu erstellen.
4. Thread Safe Singleton
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
Die vorherige Implementierung funktioniert gut und bietet Thread-Sicherheit, aber sie verringert die Leistung aufgrund der Kosten, die mit der synchronisierten Methode verbunden sind, obwohl wir sie nur für die ersten wenigen Threads benötigen, die separate Instanzen erstellen könnten. Um diesen zusätzlichen Overhead jedes Mal zu vermeiden, wird das Prinzip des double-checked Lockings verwendet. Bei diesem Ansatz wird der synchronisierte Block innerhalb der if
-Bedingung mit einer zusätzlichen Überprüfung verwendet, um sicherzustellen, dass nur eine Instanz einer Singleton-Klasse erstellt wird. Der folgende Codeausschnitt stellt die Implementierung des double-checked Lockings bereit:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Setzen Sie Ihr Lernen mit Thread Safe Singleton Class fort.
5. Bill Pugh Singleton Implementierung
Vor Java 5 hatte das Java-Speichermodell viele Probleme, und die früheren Ansätze versagten in bestimmten Szenarien, in denen zu viele Threads gleichzeitig die Instanz der Singleton-Klasse erhalten wollten. Also kam Bill Pugh mit einem anderen Ansatz, um die Singleton-Klasse mithilfe einer inneren statischen Hilfsklasse zu erstellen. Hier ist ein Beispiel für die Bill Pugh Singleton-Implementierung:
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Beachten Sie die private innere statische Klasse, die die Instanz der Singleton-Klasse enthält. Wenn die Singleton-Klasse geladen wird, wird die SingletonHelper
-Klasse nicht in den Speicher geladen, und nur wenn jemand die getInstance()
-Methode aufruft, wird diese Klasse geladen und erstellt die Singleton-Klasseninstanz. Dies ist der am häufigsten verwendete Ansatz für die Singleton-Klasse, da er keine Synchronisierung erfordert.
6. Verwendung von Reflection, um das Singleton-Muster zu zerstören
Reflection kann verwendet werden, um alle bisherigen Singleton-Implementierungsansätze zu zerstören. Hier ist eine Beispielklasse:
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// Dieser Code wird das Singleton-Muster zerstören
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
Wenn Sie die vorherige Testklasse ausführen, werden Sie feststellen, dass der hashCode
beider Instanzen nicht gleich ist, was das Singleton-Muster zerstört. Reflexion ist sehr leistungsfähig und wird in vielen Frameworks wie Spring und Hibernate verwendet. Setzen Sie Ihr Lernen mit dem Java-Reflexion-Tutorial fort.
7. Enum Singleton
Um diese Situation mit Reflexion zu überwinden, schlägt Joshua Bloch die Verwendung von enum
vor, um das Singleton-Entwurfsmuster zu implementieren, da Java sicherstellt, dass jeder enum
-Wert in einem Java-Programm nur einmal instanziiert wird. Da Java Enum-Werte global zugänglich sind, gilt dies auch für das Singleton. Der Nachteil ist, dass der enum
-Typ etwas unflexibel ist (zum Beispiel erlaubt er keine verzögerte Initialisierung).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// etwas tun
}
}
8. Serialisierung und Singleton
Manchmal müssen wir in verteilten Systemen das Serializable
-Interface in der Singleton-Klasse implementieren, damit wir ihren Zustand im Dateisystem speichern und zu einem späteren Zeitpunkt abrufen können. Hier ist eine kleine Singleton-Klasse, die auch das Serializable
-Interface implementiert:
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}
Das Problem mit serialisierten Singleton-Klassen besteht darin, dass jedes Mal, wenn wir sie deserialisieren, eine neue Instanz der Klasse erstellt wird. Hier ist ein Beispiel:
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
// von Datei zu Objekt deserialisieren
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
Dieser Code erzeugt diese Ausgabe:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
Das zerstört das Singleton-Muster. Um dieses Szenario zu überwinden, müssen wir lediglich die Implementierung der readResolve()
-Methode bereitstellen.
protected Object readResolve() {
return getInstance();
}
Nach diesem Schritt wird festgestellt, dass der hashCode
beider Instanzen im Testprogramm identisch ist.
Informieren Sie sich über Java Serialization und Java Deserialization.
Fazit
Dieser Artikel behandelte das Singleton-Entwurfsmuster.
Weiterführendes Lernen mit mehr Java-Tutorials.