The Singleton Pattern is one of the most commonly used design patterns in Java. It ensures that a class has only one instance and provides a global point of access to that instance.
Think of it like a manager in a team, there is only one manager for a specific team, and everyone in the other department communicates through them.
Let’s break it down in simple terms and understand how you can implement this in Java.
Why Use the Singleton Pattern?
-
Single Instance: To ensure that there is only one instance of a class in your application.
- For example, think of a database connection pool, having a single instance ensures efficient reuse of connections without repeatedly creating and destroying them.
- Another example is a printer spooler that manages print jobs across the entire system, ensuring there’s no conflict between users.
Global Access Point: To provide a single, shared point of access to the instance.
Resource Management: To manage shared resources like configurations, logging, or thread pools efficiently.
How to Implement Singleton Pattern in Java
1. Lazy Initialization Singleton
This approach initializes the instance only when it’s needed.
import java.io.Serializable;
public class LazySingleton implements Serializable {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
Visualization:
Imagine a library where a librarian only sets up a table when a visitor arrives:
- The
if (instance == null)
check is like the librarian checking if there’s already a table set up. - If no table is set, they quickly arrange one (create the instance).
- The next visitors reuse the same table without additional setup.
Problems and Fixes:
-
Serialization Issue:
-
Breaking Example: Without
readResolve
, deserialization creates a new instance.
import java.io.*; public class TestSerialization { public static void main(String[] args) throws Exception { LazySingleton instance1 = LazySingleton.getInstance(); // Serialize the instance ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser")); oos.writeObject(instance1); oos.close(); // Deserialize the instance ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser")); LazySingleton instance2 = (LazySingleton) ois.readObject(); ois.close(); System.out.println(instance1 == instance2); // false } }
-
Fix: Add
readResolve
to ensure the same instance is returned.
private Object readResolve() { return getInstance(); }
-
Breaking Example: Without
-
Reflection Issue:
- Breaking Example: Reflection can call the private constructor.
import java.lang.reflect.Constructor; public class TestReflection { public static void main(String[] args) throws Exception { LazySingleton instance1 = LazySingleton.getInstance(); Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); LazySingleton instance2 = constructor.newInstance(); System.out.println(instance1 == instance2); // false } }
- Fix: Add a guard in the constructor to throw an exception if an instance already exists.
private LazySingleton() { if (instance != null) { throw new IllegalStateException("Instance already created"); } }
2. Thread-Safe Singleton
To make the Singleton thread-safe, you can use the synchronized
keyword.
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
Visualization:
Imagine a bakery where only one oven can bake bread at a time:
- The
synchronized
keyword acts as a lock on the oven, ensuring only one baker can use it to bake bread. - When the first loaf (instance) is ready, all other bakers can simply reuse it.
Problems and Fixes:
-
Serialization Issue:
-
Breaking Example: Without
readResolve
, deserialization creates a new instance.
public class TestPerformance { public static void main(String[] args) { Runnable task = () -> { for (int i = 0; i < 1000; i++) { ThreadSafeSingleton.getInstance(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); long start = System.nanoTime(); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.nanoTime(); System.out.println("Execution Time: " + (end - start) + " ns"); } }
- Fix: Use double-checked locking to reduce synchronization overhead:
public static ThreadSafeSingleton getInstance() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }
-
Breaking Example: Without
-
Reflection Issue:
- Fix: Add a guard in the constructor:
private ThreadSafeSingleton() { if (instance != null) { throw new IllegalStateException("Instance already created"); } }
3. Double-Checked Locking Singleton
This approach minimizes the performance cost of synchronization.
public class DoubleCheckedLockingSingleton {
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {}
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
Visualization:
Imagine a bank vault:
- The first
if
condition is like a security camera checking if the vault is already open. - The
synchronized
block is like a guard only letting one person unlock the vault at a time. - The second
if
ensures the vault isn’t opened twice by mistake.
Problems and Fixes:
-
Serialization Issue:
- Same fix as Lazy Singleton.
-
Reflection Issue:
- Same fix as Lazy Singleton.
4. Bill Pugh Singleton (Recommended)
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Visualization:
Think of a vending machine with a coin slot that automatically creates change when needed:
- The
SingletonHelper
class is like a hidden compartment in the vending machine, pre-loaded with the mechanism to give change. - Only when someone inserts a coin (calls
getInstance
) does the mechanism activate, dispensing the Singleton instance.
Problems and Fixes:
-
Serialization Issue:
-
Breaking Example: Without
readResolve
, deserialization creates a new instance.
import java.io.*; public class TestSerialization { public static void main(String[] args) throws Exception { BillPughSingleton instance1 = BillPughSingleton.getInstance(); // Serialize the instance ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser")); oos.writeObject(instance1); oos.close(); // Deserialize the instance ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser")); BillPughSingleton instance2 = (BillPughSingleton) ois.readObject(); ois.close(); System.out.println(instance1 == instance2); // false } }
-
Fix: Add
readResolve
to ensure the same instance is returned.
private Object readResolve() { return getInstance(); }
-
Breaking Example: Without
-
Reflection Issue:
- Fix: Add a guard in the constructor:
private BillPughSingleton() { if (SingletonHelper.INSTANCE != null) { throw new IllegalStateException("Instance already created"); } }
5. Singleton Using Enum (Modern Approach)
With newer Java versions (Java 8 and beyond), you can use Enums to implement Singleton. Enums are inherently thread-safe and handle serialization by default.
public enum EnumSingleton {
INSTANCE;
public void showMessage() {
System.out.println("Hello from Singleton!");
}
}
Visualization:
Picture a monarch (king or queen) who is the sole ruler of their kingdom:
- The
INSTANCE
is like the monarch, inherently unique and impossible to duplicate. - The JVM ensures that no matter how many times you call the monarch, you always get the same ruler.
Final Versions of Singleton Patterns with fixes
1. Lazy Initialization Singleton
import java.io.*;
public class LazySingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static LazySingleton instance;
private LazySingleton() {
if (instance != null) {
throw new IllegalStateException("Instance already created");
}
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
private Object readResolve() {
return getInstance();
}
}
2. Thread-Safe Singleton
import java.io.*;
public class ThreadSafeSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
if (instance != null) {
throw new IllegalStateException("Instance already created");
}
}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
private Object readResolve() {
return getInstance();
}
}
3. Double-Checked Locking Singleton
import java.io.*;
public class DoubleCheckedLockingSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
if (instance != null) {
throw new IllegalStateException("Instance already created");
}
}
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
private Object readResolve() {
return getInstance();
}
}
4. Bill Pugh Singleton (Recommended)
import java.io.*;
public class BillPughSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private BillPughSingleton() {
if (SingletonHelper.INSTANCE != null) {
throw new IllegalStateException("Instance already created");
}
}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
private Object readResolve() {
return getInstance();
}
}
5. Singleton Using Enum (Modern Approach)
public enum EnumSingleton {
INSTANCE;
public void showMessage() {
System.out.println("Hello from Enum Singleton!");
}
}
Summary
- Lazy Initialization: Simple, but not thread-safe without additional fixes.
- Thread-Safe Singleton: Ensures thread safety but may incur performance overhead.
- Double-Checked Locking: Reduces synchronization overhead while remaining thread-safe.
- Bill Pugh Singleton: Recommended for its simplicity, efficiency, and elegance.
- Enum Singleton: Modern and robust, suitable for most use cases.
Top comments (0)