The Singleton Design Pattern: A widely-used and classic design pattern. When a class is designed as a singleton, it ensures that only one instance of that class can exist within an application. Typically, we employ this pattern when we need a single global access point to that instance.
Table of Contents
1. How to create a singleton class
To make a class a singleton, you should follow these steps:
a) Declare the class constructor as private: By declaring the class constructor as private, you prevent other classes in the application from creating objects of the class directly. This ensures that only one instance is allowed.
b) Create a static method: Since the constructor is private, external classes cannot directly call it to create objects. To overcome this, you can create a static method within the class. This method contains the logic for checking and returning a single object of the class. Since it’s a static method, it can be called without the need for an object. This method is often referred to as a factory method or static factory method.
c) Declare a static member variable of the same class type: In the static method mentioned above, you need to keep track of whether an object of the class already exists. To achieve this, you initially create an object and store it in a member variable. In subsequent calls to the method, you return the same object stored in the member variable. However, member variables cannot be accessed directly in static methods, so you declare the member variable as a static variable to hold the reference to the class’s single instance.
Here’s a sample piece of code to illustrate these concepts:
The UML representation of the singleton pattern is as follows:
Important points to keep in mind:
- The
CacheManager()
constructor is declared as private. - The class contains a static variable named
instance
. - The
getInstance()
method is static and serves as a factory method for creating instances of the class.
public class CacheManager {
// Declare a static member of the same class type.
private static CacheManager instance;
// Private constructor to prevent other classes from creating objects.
private CacheManager() {
}
// Declare a static method to create only one instance.
private static CacheManager getInstance() {
if (instance == null) {
instance = new CacheManager();
}
return instance;
}
}
We can express the above code in various alternative ways, and there are numerous methods to enhance its implementation. Let’s explore some of those approaches in the sections below.
1.1 Eager Initialization
In the previous code, we instantiated the instance
on the first call to the getInstance()
method. Instead of deferring instantiation until the method is called, we can initialize it eagerly, well before the class is loaded into memory, as demonstrated below:
public class CacheManager {
// Instantiate the instance object during class loading.
private static CacheManager instance = new CacheManager();
private CacheManager() {
}
private static CacheManager getInstance() {
return instance;
}
}
1.2 Static Block Initialization
If you are familiar with the concept of static blocks in Java, you can utilize this concept to instantiate the singleton class, as demonstrated below:
public class CacheManager {
private static CacheManager instance;
// The static block executes only once when the class is loaded.
static {
instance = new CacheManager();
}
private CacheManager() {
}
private static CacheManager getInstance() {
return instance;
}
}
However, the drawback of the above code is that it instantiates the object even when it’s not needed, during class loading.
1.3 Lazy Initialization
In many cases, it’s advisable to postpone the creation of an object until it’s actually needed. To achieve this, we can delay the instantiation process until the first call to the getInstance()
method. However, a challenge arises in a multithreaded environment when multiple threads are executing simultaneously; it might lead to the creation of more than one instance of the class. To prevent this, we can declare the getInstance()
method as synchronized.
1.4 Override clone()
Method and Throw CloneNotSupportedException
To prevent a singleton class from being cloneable, it is recommended to implement the class from the Cloneable
interface and override the clone()
method. Within this method, we should throw CloneNotSupportedException
to prevent cloning of the object. The clone()
method in the Object
class is protected and not visible outside the class, unless it is overridden. So, it’s important to implement Cloneable
and throw an exception in the clone()
method.
However, there’s a problem with the above code. After the first call to getInstance()
, subsequent calls to the method will still check the instance == null
condition, even though it’s not necessary. Acquiring and releasing locks are costly operations, and we should minimize them. To address this issue, we can implement a double-check for the condition.
Additionally, it’s recommended to declare the static member instance
as volatile to ensure thread-safety in a multi-threaded environment.
1.5 Serialization and Deserialization Issue
Serialization and deserialization of a singleton class can create multiple instances, violating the singleton rule. To address this, we need to implement the readResolve()
method within the singleton class. During the deserialization process, the readResolve()
method is called to reconstruct the object from the byte stream. By implementing this method and returning the same instance, we can avoid the creation of multiple objects even during serialization and deserialization.
Now, let’s revisit the provided code to address the issue:
public class CacheSerialization {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
CacheManager cacheManager1 = CacheManager.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
new File("D:\\cacheManager.ser")));
oos.writeObject(cacheManager1);
CacheManager cacheManager2 = null;
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File("D:\\cacheManager.ser")));
cacheManager2 = (CacheManager) ois.readObject();
System.out.println("cacheManager1 == cacheManager2 : " + (cacheManager1 == cacheManager2)); // false
}
}
In this code, you’re experiencing an issue where cacheManager1
and cacheManager2
instances do not behave as expected after deserialization it return false. This discrepancy indicates the creation of duplicate objects, which contradicts the desired behavior of a singleton pattern.
To resolve this issue, you can rectify your CacheManager
class by adding a readResolve()
method. This method ensures that only one instance is maintained throughout the deserialization process, thereby preserving the correct behavior of the singleton pattern.
Here is the final version of the singleton class, which addresses all the relevant cases:
import java.io.Serializable;
public class CacheManager implements Serializable, Cloneable {
private static volatile CacheManager instance;
// Private constructor to prevent external instantiation.
private CacheManager() {
}
// Method to retrieve the singleton instance.
private static CacheManager getInstance() {
if (instance == null) {
synchronized (CacheManager.class) {
// Double-check to ensure a single instance is created.
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
// This method is called during deserialization to return the existing instance.
public Object readResolve() {
return instance;
}
// Prevent cloning by throwing CloneNotSupportedException.
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
In conclusion, the provided code defines a robust implementation of the Singleton Design Pattern in Java. It guarantees that only one instance of the CacheManager
class is created, even in multithreaded environments, thanks to double-checked locking and the use of the volatile
keyword.
Moreover, it addresses potential issues with serialization and deserialization by implementing the readResolve()
method, ensuring that only a single instance is maintained throughout the object’s lifecycle. Additionally, it prevents cloning of the singleton object by throwing CloneNotSupportedException
in the clone()
method.
Conclusion: Ensuring Singleton Design Pattern Best Practices
In summary, this code exemplifies a well-rounded approach to creating and safeguarding a singleton class while adhering to best practices and design principles.