Singleton Design Pattern with Examples in Java
The Singleton design pattern is used when we need to ensure that only one instance of a particular class can be created throughout the lifetime of an application.
Purpose and Usage
Single Instance: Wherever the class is required, the same single instance will be used instead of creating a new object of the class.
Resource Efficiency: Creating objects consumes resources. If an application can function with a single instance of a particular class, using the Singleton pattern can improve efficiency.
Real-World Applications: Common uses of the Singleton pattern include:
Creating a database connection object only once
Maintaining a single logger object throughout the Java Virtual Machine (JVM)
Managing a single configuration manager object
Implementation Approaches
There are several ways to implement the Singleton pattern. Let's explore a few:
1. Simple (Non-Thread Safe) Approach
This approach is straightforward but not thread-safe:
public class DBConnection {
private static DBConnection instance = null;
private DBConnection() {
// Private constructor to prevent instantiation
}
public static DBConnection getInstance() {
if (instance == null) {
instance = new DBConnection();
}
return instance;
}
}
Key Points:
The constructor is private, preventing external instantiation.
The
getInstance()
method is static, allowing it to be called without creating an object.The
if
condition ensures only one object is created.
Limitation: This implementation is not thread-safe.
2. Eager Initialization Approach
In this approach, we initialize the instance at class loading time:
public class DBConnection {
private static final DBConnection instance = new DBConnection();
private DBConnection() {
// Private constructor
}
public static DBConnection getInstance() {
return instance;
}
}
Advantages:
- Thread-safe without explicit synchronization
Concerns:
Increases application start time as the object is created during class loading.
May waste resources if the instance is never used.
Cannot pass runtime parameters to the constructor.
3. Thread Safe Approach with Synchronized at method level and its Disadvantages:
okay so in the First method using if-else statements, lets try to put a Synchronized keyword while declaring the method.
public class DBConnection {
private static DBConnection instance = null;
private DBConnection() {
// Private constructor to prevent instantiation
}
public static synchronized DBConnection getInstance() {
if (instance == null) {
instance = new DBConnection();
}
return instance;
}
}
we know that the synchronized keyword dosent allow multiple threads to enter the method at once, it allows one by one thread.
so lets clearly think about two situations:
During Object Creation Imagine there are 10 threads trying to enter the
getInstance()
method when the instance hasn't been created yet:The
synchronized
keyword allows only one thread to enter the method at a time.Each thread, one after another, will attempt to create the instance.
This could potentially lead to the same instance being created multiple times, defeating the purpose of the Singleton pattern.
After Object Creation Now consider a scenario where the Singleton instance has already been created, and 1000 threads are trying to access it:
Again, the
synchronized
keyword forces these 1000 threads to enter the method one by one.Even though the instance already exists and no creation is needed, each thread still has to wait its turn.
This unnecessary serialization significantly slows down the application.
So doing this is of No use.
4. Thread Safe Approach with double-checked locking:
Okay, so we know the synchronized keyword can slow things down. But what should we do to make it faster?
class DBConn {
private static volatile DBConn instance;
private DBConn() {}
public static DBConn getInstance() {
if (instance == null) {
synchronized(DBConn.class) {
if (instance == null) {
instance = new DBConn();
}
}
}
return instance;
}
}
Explanation of the above approach:
→ We're checking if the instance is null twice. Why? It's like a double security check.
→ Let's say 1000 threads hit the getInstance method at once. The first check (outside the synchronized block) sees if the instance already exists.
→ When the instance is null, things get interesting. The synchronized block is like a narrow door - only one thread can squeeze through at a time.
→ So, thread #1 gets in and sees instance is still null. It goes ahead and creates the object. While it's doing that, threads #2 to #1000 are waiting their turn.
→ Now, thread #1 is done and leaves the synchronized block. Thread #2 gets in, but the instance isn't null anymore! So it just shrugs and leaves without creating anything.
→ Same thing happens for all the other threads. They check, see the instance exists, and move on.
This diagram shows how different threads interact with the getInstance() method. Thread 1 creates the instance, while Threads 2 and 3 (representing all subsequent threads) just get the existing instance.
And that 'volatile' keyword in the above code?
It's there to make sure all threads see the latest value of instance. Without it, some threads might see an old, null value due to memory caching stuff.
5. Enum Singleton:
A super simple way to do Singleton? Use an enum:
public enum DBConn {
INSTANCE;
public void connect() {
// Do database connection stuff
}
}
DBConn.INSTANCE.connect();
ENUMS are created and initialized only once in java throughout the JVM. cool right?
6. Issues with Serialization and Deserialization:
You know what? when we serialize and deserialize an object we donot endup with the same object that we serializedu.
But that's not what we want in a Singleton right?
To fix this, add this method to your Singleton class:
private Object readResolve() {
return getInstance();
}
This tells Java to use your getInstance() method when deserializing, so you always get the same instance.
7. Breaking Singleton Pattern with Reflections:
A private constructor can be made available during runtime using Reflections in Java.
DBConn instance1 = DBConn.getInstance();
DBConn instance2 = null;
Constructor[] constructors = DBConn.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
instance2 = (DBConn) constructor.newInstance();
break;
}
Now instance1 and instance2 are different objects!
To prevent this, you could throw an exception in the constructor if an instance already exists. But remember, if someone really wants to break your Singleton, they probably can. It's more about making it clear that they shouldn't, rather than making it impossible.
Want to read more? go for these:
Subscribe to my newsletter
Read articles from Shamitha Reddy Regenti directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Shamitha Reddy Regenti
Shamitha Reddy Regenti
Hey everyone, I'm Shamitha, working as a programmer analyst at amazon. and also i teach DSA and AWS in a practical way. look me up at teacheron!