Inleiding
Het Java Singleton Patroon is een van de Gangs of Four Ontwerppatronen en valt onder de categorie Creational Design Pattern. Vanuit de definitie lijkt het een eenvoudig ontwerppatroon te zijn, maar als het op implementatie aankomt, brengt het veel zorgen met zich mee.
In dit artikel zullen we de principes van het singleton ontwerppatroon leren, verschillende manieren verkennen om het singleton ontwerppatroon te implementeren, en enkele van de beste praktijken voor het gebruik ervan.
Principes van het Singleton Patroon
- Het singleton patroon beperkt de instantiatie van een klasse en zorgt ervoor dat er slechts één instantie van de klasse bestaat in de Java Virtual Machine.
- De singleton klasse moet een wereldwijd toegangspunt bieden om de instantie van de klasse te verkrijgen.
- Het singleton patroon wordt gebruikt voor logging, driver objecten, caching, en thread pool.
- Singleton ontwerppatroon wordt ook gebruikt in andere ontwerppatronen zoals Abstract Factory, Builder, Prototype, Facade, enz.
- Singleton ontwerppatroon wordt ook gebruikt in kern Java-klassen (bijvoorbeeld
java.lang.Runtime
,java.awt.Desktop
).
Implementatie van het Singleton-patroon in Java
Om een singleton patroon te implementeren, zijn er verschillende benaderingen, maar ze hebben allemaal de volgende gemeenschappelijke concepten.
- Private constructor om de instantiatie van de klasse vanuit andere klassen te beperken.
- Private statische variabele van dezelfde klasse die de enige instantie van de klasse is.
- Openbare statische methode die de instantie van de klasse retourneert, dit is het wereldwijde toegangspunt voor de buitenwereld om de instantie van de singletonklasse te verkrijgen.
In de volgende secties zullen we verschillende benaderingen van de implementatie van het singleton patroon leren en ontwerpoverwegingen bij de implementatie.
1. Gretige initialisatie
Bij gretige initialisatie wordt de instantie van de singleton-klasse gemaakt op het moment van het laden van de klasse. Het nadeel van gretige initialisatie is dat de methode wordt gemaakt, zelfs als de toepassing van de client deze misschien niet gebruikt. Hier is de implementatie van de singleton-klasse met statische initialisatie:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// privéconstructor om te voorkomen dat clienttoepassingen de constructor gebruiken
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
Als uw singleton-klasse niet veel middelen gebruikt, is dit de aanpak om te gebruiken. Maar in de meeste gevallen worden singleton-klassen gemaakt voor bronnen zoals het bestandssysteem, databaseverbindingen, enzovoort. We moeten de instantiatie vermijden, tenzij de client de getInstance
-methode aanroept. Ook biedt deze methode geen opties voor uitzonderingsafhandeling.
2. Statische blokinitialisatie
De implementatie van de initialisatie van een statisch blok is vergelijkbaar met een gretige initialisatie, behalve dat er een instantie van de klasse wordt aangemaakt in het statische blok dat de mogelijkheid biedt voor uitzonderingsafhandeling.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// statische blokinitialisatie voor uitzonderingsafhandeling
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
Zowel gretige initialisatie als initialisatie via een statisch blok maken de instantie aan voordat deze wordt gebruikt, en dat is niet de beste praktijk om te gebruiken.
3. Luie initialisatie
De methode van luie initialisatie om het singletonpatroon te implementeren, maakt de instantie aan in de methode voor wereldwijde toegang. Hier is de voorbeeldcode voor het maken van de singletonklasse met deze aanpak:
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;
}
}
De bovenstaande implementatie werkt prima in het geval van een single-threaded omgeving, maar wanneer het gaat om multi-threaded systemen, kan het problemen veroorzaken als meerdere threads zich tegelijkertijd binnen de if
-voorwaarde bevinden. Het zal het singletonpatroon vernietigen en beide threads zullen verschillende instanties van de singletonklasse krijgen. In het volgende gedeelte zullen we verschillende manieren zien om een thread-safe singletonklasse te maken.
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;
}
}
De voorgaande implementatie werkt prima en zorgt voor thread-veiligheid, maar het vermindert de prestaties vanwege de kosten die gepaard gaan met de gesynchroniseerde methode, hoewel we deze alleen nodig hebben voor de eerste paar threads die mogelijk afzonderlijke instanties kunnen maken. Om deze extra overhead elke keer te vermijden, wordt het principe van dubbel gecontroleerd vergrendelen gebruikt. In deze benadering wordt het gesynchroniseerde blok gebruikt binnen de if
-voorwaarde met een extra controle om ervoor te zorgen dat slechts één instantie van een singletonklasse wordt gemaakt. De volgende codefragment biedt de implementatie van dubbel gecontroleerd vergrendelen:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Ga verder met leren met Thread Safe Singleton Class.
5. Bill Pugh Singleton-implementatie
Vóór Java 5 had het Java-geheugenmodel veel problemen, en de vorige benaderingen faalden in bepaalde scenario’s waar te veel threads tegelijkertijd probeerden de instantie van de singletonklasse te krijgen. Dus kwam Bill Pugh met een andere benadering om de singletonklasse te maken met behulp van een innerlijke statische hulpprogrammaklasse. Hier is een voorbeeld van de implementatie van de Bill Pugh Singleton:
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;
}
}
Let op de private innerlijke statische klasse die de instantie van de singletonklasse bevat. Wanneer de singletonklasse wordt geladen, wordt de klasse SingletonHelper
niet in het geheugen geladen en pas wanneer iemand de methode getInstance()
aanroept, wordt deze klasse geladen en wordt de instantie van de singletonklasse gemaakt. Dit is de meest gebruikte benadering voor de singletonklasse omdat deze geen synchronisatie vereist.
6. Gebruik van Reflectie om Singleton Patroon te vernietigen
Reflectie kan worden gebruikt om alle vorige singleton-implementatiebenaderingen te vernietigen. Hier is een voorbeeldklasse:
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) {
// Deze code zal het singletonpatroon vernietigen
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
Wanneer je de bovenstaande testklasse uitvoert, zul je opmerken dat de hashCode
van beide instanties niet hetzelfde is, wat het singleton-patroon tenietdoet. Reflectie is zeer krachtig en wordt veel gebruikt in frameworks zoals Spring en Hibernate. Ga verder met leren via Java Reflection Tutorial.
7. Enum Singleton
Om deze situatie met Reflectie te overwinnen, stelt Joshua Bloch voor om enum
te gebruiken om het singleton-ontwerppatroon te implementeren, aangezien Java ervoor zorgt dat elke enum
-waarde slechts één keer wordt geïnstantieerd in een Java-programma. Aangezien waarden van Java Enum wereldwijd toegankelijk zijn, geldt dat ook voor het singleton-patroon. Het nadeel is dat het enum
-type enigszins inflexibel is (het staat bijvoorbeeld geen luie initialisatie toe).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// doe iets
}
}
8. Serialisatie en Singleton
Soms moeten we in gedistribueerde systemen de Serializable
-interface implementeren in de singleton-klasse, zodat we de staat ervan kunnen opslaan in het bestandssysteem en later kunnen ophalen. Hier is een kleine singleton-klasse die ook de Serializable
-interface implementeert:
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;
}
}
Het probleem met de geserialiseerde singleton-klasse is dat telkens wanneer we deze deserialiseren, er een nieuw exemplaar van de klasse wordt gemaakt. Hier is een voorbeeld:
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();
// deserialize van bestand naar object
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());
}
}
Dit levert deze uitvoer op:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
Dus het vernietigt het singletonpatroon. Om deze situatie te overwinnen, hoeven we alleen maar de implementatie van de methode readResolve()
te verstrekken.
protected Object readResolve() {
return getInstance();
}
Daarna zul je merken dat de hashCode
van beide instanties hetzelfde is in het testprogramma.
Lees meer over Java Serialisatie en Java Deserialisatie.
Conclusie
Dit artikel behandelde het singleton-ontwerppatroon.
Ga verder met leren met meer Java tutorials.