➤ How to Code a Game
➤ Array Programs in Java
➤ Java Inline Thread Creation
➤ Java Custom Exception
➤ Hibernate vs JDBC
➤ Object Relational Mapping
➤ Check Oracle DB Size
➤ Check Oracle DB Version
➤ Generation of Computers
➤ XML Pros & Cons
➤ Git Analytics & Its Uses
➤ Top Skills for Cloud Professional
➤ How to Hire Best Candidates
➤ Scrum Master Roles & Work
➤ CyberSecurity in Python
➤ Protect from Cyber-Attack
➤ Solve App Development Challenges
➤ Top Chrome Extensions for Twitch Users
➤ Mistakes That Can Ruin Your Test Metric Program
Singleton Design Pattern | The Singleton pattern is one of the simplest design patterns in Java. This pattern involves a single class that is responsible for creating an object while making sure that only a single object gets created in one JVM.
This class provides a way to access its only object which can be accessed directly without the need to instantiate the object of the class.
Singleton class is a class whose only one instance can be created at any given time, in one JVM. A simple Singleton class can be written as:-
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Test {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
System.out.println(singleton1.hashCode());
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton2.hashCode());
}
}
Output:-
114935352
114935352
Whereas a complete class may look like this:-
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
if (instance != null) {
throw new IllegalStateException("Singleton instance already exists. "+
"Use getInstance() method");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
// double check
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Let us see why we have used volatile keyword, synchronization concept with block scope, double checking, and why the constructor is throwing an exception.
Table of Contents
Eager Initialization and Lazy Initialization of Singleton Class
Both of these implementations are ways to create a Singleton pattern in Java, but they have different characteristics and considerations.
Eager Initialization:-
public class Singleton {
// Make it private and final then Initialize the object
private static final Singleton instance = new Singleton();
// Make constructor private
private Singleton() {
}
// return instance
public static Singleton getInstance() {
return instance ;
}
}
This implementation is called “Eager Initialization” because it initializes the Singleton instance at the time of class loading. This ensures thread safety but may consume unnecessary resources if the Singleton object is never used.
Lazy Initialization:-
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
This implementation is called “Lazy Initialization” because the Singleton instance is created only when it is requested for the first time. This conserves resources because the object is not created until it is needed. However, this implementation is not thread-safe in its current form. If multiple threads access the getInstance() method simultaneously, it could result in the creation of multiple instances.
To make the second implementation thread-safe, we can use synchronization:-
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Double-Checked Locking in Singleton Class
In the above implementation using synchronization, though it’s thread-safe and solves the issue of multiple instances, isn’t very efficient. We need to bear the cost of synchronization every time we call the getInstance() method, while synchronization is only needed the first time when the Singleton instance is created.
When the first two threads t1 and t2 tried to access the getInstance() method, then only one thread could check whether the instance was null, and it could create a new instance. Meanwhile, the whole method will be locked and thread t2 have to wait. Now, thread t1 had already created the instance, and thread t3 and t4 came simultaneously to get the instance. Even though instances exist but due to the synchronized method they have to access the method one by one, and the cost of locking and unlocking will be involved.
Therefore the performance is impacted by making the whole method synchronized. In a multi-threaded environment, instead of synchronizing the whole method, we should make only that part of the code synchronized which was creating the problem. This brings us to a double-checked locking pattern, where only a critical section of code is locked.
It is called double-checked locking because there are two checks for instance == null, one without locking and the other with locking (inside synchronized) block.
if (instance == null) {
synchronized (Singleton.class){
// double checked
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
Here the Intention is to reduce the cost of synchronization and improve performance, by only locking critical sections of code, the code that creates an instance of Singleton class.
Now only the first time (thread t1) code goes in the synchronized block and for the rest all the calls ((thread t2, t3, t4), the code is not synchronized, and hence performance increases in this implementation.
Potential Issue in Multithreaded Environment
1. Issue due to context switching
In a multi-core system, the provided implementation can still break due to instruction reordering and the lack of proper memory visibility guarantees. This is because the instance reference might be visible to other threads before the constructor of Singleton is completed.
Problem Scenario:-
- Thread A enters the getInstance() method.
- The
instance
is null, so it enters the synchronized block. - Thread A passes the
if (instance == null)
check of synchronized lock. - Thread A is preempted (context switch happens), and Thread B enters the getInstance() method.
- Thread B sees instance is still null and enters the synchronized block.
- Thread B creates a new Singleton instance and assigns it to the instance.
- Thread A resumes and also creates a new Singleton instance and assigns it to the instance.
This results in two different instances of Singleton being created and assigned to the instance, violating the Singleton property.
2. Issue due to memory caching in a multicore system
When dealing with multithreading in a multicore system, memory visibility issues can arise due to how different cores handle caching and memory access. Let’s delve into what can happen if Thread1 stores the instance in one core and Thread2 accesses the instance from another core without the proper use of synchronization and memory visibility guarantees.
Memory Caching in Multicore Systems:- In a multicore system, each core typically has its own local cache. When Thread1 running on Core1 updates the instance variable, the update is initially written to Core1’s cache. If Thread2 is running on Core2, it may not immediately see the update made by Thread1 because Core2 might still have an old value of instance in its local cache. This leads to a stale read problem.
Problem Scenario:-
- Thread1 (on Core1) calls getInstance().
- Thread1 sees instance is null and enters the synchronized block.
- Thread1 creates a new Singleton instance and assigns it to instance.
- The new instance reference is written to Core1’s cache.
- Thread1 exits the synchronized block and returns the new Singleton instance.
- Thread2 (on Core2) calls getInstance().
- Thread2 may still see instance as null if Core2 has not yet seen the update from Core1.
- Thread2 also enters the synchronized block and creates a new Singleton instance.
- Thread2 assigns its new Singleton instance to instance.
Java Memory Model and Volatile
The Java Memory Model (JMM) defines how threads interact through memory and provides guarantees about the visibility and ordering of memory operations. Using the volatile keyword ensures that:
- Visibility: A write to a volatile variable in one thread will be visible to reads of that volatile variable in other threads.
- Ordering: Operations before writing to a volatile variable in the code will happen before subsequent reads of that volatile variable.
The Java volatile keyword is used to mark a Java variable as “being stored in main memory“. More precisely that means, that every read of a volatile variable will be read from the computer’s main memory, and not from the CPU cache, and that every write to a volatile variable will be written to the main memory, and not just to the CPU cache.
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;
}
}
- Thread1 (on Core1) calls getInstance().
- Thread1 sees instance is null and enters the synchronized block.
- Thread1 creates a new Singleton instance and assigns it to the instance.
- The volatile keyword ensures the write-to instance is immediately visible to other threads.
- Thread2 (on Core2) calls getInstance().
- Thread2 sees the updated instance value from Thread1 due to the volatile guarantee.
- Thread2 returns with the already created Singleton instance without creating a new one.
Different Ways to Break Singleton Design Pattern
What are the different ways we can break a singleton design pattern in Java? There are basically 4 ways to break singleton design pattern:-
- Reflection
- Serialization
- Cloning
- By Executor Service
Breaking Singleton’s Behavior Using Reflection
public class Test {
public static void main(String[] args)
throws ClassNotFoundException, NoSuchMethodException,
SecurityException, InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
Singleton originalSingleton = Singleton.getInstance();
System.out.println(originalSingleton.hashCode());
// Breaking singleton using Reflection
Class<?> singletonClass = Class.forName("Singleton");
// or
// Class<?> singletonClass = Singleton.class;
Constructor<Singleton> constuctor =
(Constructor<Singleton>) singletonClass.getDeclaredConstructor();
constuctor.setAccessible(true);
Singleton reflectionSingleton = constuctor.newInstance();
System.out.println(reflectionSingleton.hashCode());
}
}
To prevent reflection from breaking the singleton behavior, in the constructor check whether the instance is already present or not. If the instance is already present then throw an exception.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
if(instance != null) {
throw new IllegalStateException("Singleton instance already exist. "+
"Use getInstance() method");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Breaking Singleton’s Behavior Using Serialization
The singleton class must implement the Serializable interface.
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
// remaining code
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton originalSingleton = Singleton.getInstance();
System.out.println(originalSingleton.hashCode());
// serialization process
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(new FileOutputStream("Serilization.ser"));
objectOutputStream.writeObject(originalSingleton);
objectOutputStream.close();
// de-serialization process
ObjectInputStream objectInputStream =
new ObjectInputStream(new FileInputStream("Serilization.ser"));
Singleton serializationSingleton = (Singleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(serializationSingleton.hashCode());
}
}
To prevent the serialization and deserialization process from breaking the singleton behavior, you can implement the readResolve() method in your Singleton class. This method will be called during deserialization, and you can use it to ensure that the deserialization process returns the same instance of the singleton rather than creating a new one.
Here’s how you can modify your Singleton class to include the readResolve() method:-
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
// method to prevent deserialization from breaking the singleton behaviour
protected Object readResolve() {
return instance;
}
// remaining code
}
With this implementation, when an instance of Singleton is deserialized, the readResolve() method is invoked, and it returns the existing instance of the singleton instead of creating a new one. This ensures that the singleton behavior is maintained even after deserialization.
Breaking Singleton’s Behavior Using Cloning
The Singleton class must implement the Cloneable interface. The clone() method of the Object class is protected therefore we have to override it in the Singleton class.
public class Singleton implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
// other code
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Singleton originalSingleton = Singleton.getInstance();
System.out.println(originalSingleton.hashCode());
// cloning
Singleton singletonClone = (Singleton) originalSingleton.clone();
System.out.println(singletonClone.hashCode());
}
}
To prevent this, instead of calling super.clone() method we can simply return the existing instance.
@Override
protected Object clone() throws CloneNotSupportedException {
return instance;
}
If you enjoyed this post, share it with your friends. Do you want to share more information about the topic discussed above or do you find anything incorrect? Let us know in the comments. Thank you!