🧑💻 Author: RK ROY
🔒 Singleton Pattern
Ensure a class has only one instance and provide a global point of access to it.
📋 Table of Contents
- 🎯 Intent
- 🚀 Problem & Solution
- 🏗️ Structure
- 💻 Implementation
- 🌟 Real-World Examples
- ✅ Best Practices
- ❌ Common Pitfalls
- 🎤 Interview Questions
🎯 Intent
The Singleton pattern restricts the instantiation of a class to one single instance. This is useful when exactly one object is needed to coordinate actions across the system.
Key Characteristics
- ✅ Single Instance: Only one instance exists throughout the application lifecycle
- ✅ Global Access: Provides a global access point to the instance
- ✅ Lazy Initialization: Instance created only when first requested
- ✅ Thread Safety: Handles concurrent access properly
🚀 Problem & Solution
🚨 Problem
Consider these scenarios:
- Database Connection Pool: Multiple connections would waste resources
- Logger: Multiple loggers would create file conflicts
- Configuration Manager: Inconsistent settings across the application
- Print Spooler: Multiple spoolers would cause print job conflicts
💡 Solution
🏗️ Structure
UML Class Diagram
Sequence Diagram
💻 Implementation
1. 🔰 Basic Thread-Safe Implementation (Java)
java
public class Singleton {
// Volatile ensures visibility across threads
private static volatile Singleton instance;
// Private constructor prevents instantiation
private Singleton() {
// Prevent reflection attacks
if (instance != null) {
throw new RuntimeException("Use getInstance() method to create instance");
}
}
// Double-checked locking for thread safety
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// Business methods
public void doSomething() {
System.out.println("Doing something...");
}
}
2. 🚀 Enum-Based Implementation (Java)
java
public enum SingletonEnum {
INSTANCE;
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void doSomething() {
System.out.println("Enum singleton doing: " + data);
}
}
// Usage
SingletonEnum.INSTANCE.setData("Hello World");
SingletonEnum.INSTANCE.doSomething();
🌟 Real-World Examples
1. 🗄️ Database Connection Manager
java
public class DatabaseManager {
private static volatile DatabaseManager instance;
private Connection connection;
private String url = "jdbc:mysql://localhost:3306/mydb";
private DatabaseManager() {
try {
this.connection = DriverManager.getConnection(url, "user", "password");
} catch (SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
public static DatabaseManager getInstance() {
if (instance == null) {
synchronized (DatabaseManager.class) {
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
public void executeQuery(String query) {
try (Statement stmt = connection.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
// Process results...
} catch (SQLException e) {
e.printStackTrace();
}
}
}
2. 📝 Logger Implementation
java
public class Logger {
private static volatile Logger instance;
private PrintWriter writer;
private Logger() {
try {
writer = new PrintWriter(new FileWriter("application.log", true));
} catch (IOException e) {
throw new RuntimeException("Cannot create logger", e);
}
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public void log(LogLevel level, String message) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String logEntry = String.format("[%s] %s: %s", timestamp, level, message);
synchronized (writer) {
writer.println(logEntry);
writer.flush();
}
System.out.println(logEntry);
}
public enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
}
// Usage
Logger.getInstance().log(Logger.LogLevel.INFO, "Application started");
Logger.getInstance().log(Logger.LogLevel.ERROR, "Something went wrong");
✅ Best Practices
🎯 Do's
- ✅ Use double-checked locking for thread safety
- ✅ Make constructor private to prevent direct instantiation
- ✅ Handle serialization properly if needed
- ✅ Consider using enum in Java for simplicity
- ✅ Document thread-safety guarantees
- ✅ Handle exceptions in constructor properly
📊 Thread Safety Comparison
❌ Common Pitfalls
🚨 Anti-Patterns to Avoid
- Breaking Singleton with Reflection
java
// BAD: This breaks singleton
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
// instance1 != instance2 ❌
- Serialization Issues
java
// PROBLEM: Deserialization creates new instance
public class Singleton implements Serializable {
// ... singleton implementation
// SOLUTION: Override readResolve()
protected Object readResolve() {
return getInstance();
}
}
- Multithreading Issues
java
// BAD: Race condition
public static Singleton getInstance() {
if (instance == null) { // Thread 1 checks
instance = new Singleton(); // Thread 2 also creates!
}
return instance;
}
🔧 Testing Challenges
java
// PROBLEM: Hard to test due to global state
@Test
public void testSingleton() {
Singleton s1 = Singleton.getInstance();
s1.setValue("test1");
// This test might fail if another test modified the singleton
assertEquals("test1", s1.getValue());
}
// SOLUTION: Reset method for testing (not recommended for production)
public void resetForTesting() {
if (isTestEnvironment()) {
instance = null;
}
}
🎤 Interview Questions
📝 Fundamental Questions
Q1: What is the Singleton pattern and when would you use it?
A: The Singleton pattern ensures a class has only one instance and provides global access to it. Use it for:
- Database connections
- Loggers
- Configuration managers
- Thread pools
- Caches
Q2: How do you make Singleton thread-safe?
A: Several approaches:
java
// 1. Synchronized method (performance impact)
public static synchronized Singleton getInstance() { ... }
// 2. Double-checked locking (recommended)
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 3. Enum-based (best approach)
public enum Singleton {
INSTANCE;
// methods...
}
🚀 Advanced Questions
Q3: What are the problems with Singleton pattern?
A:
- Testing difficulties: Hard to mock, global state
- Violation of Single Responsibility: Class controls its creation and behavior
- Hidden dependencies: Dependencies not clear from constructor
- Concurrency issues: If not implemented properly
- Serialization problems: Can create multiple instances
Q4: How would you break a Singleton?
A:
java
// 1. Reflection
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton broken = constructor.newInstance();
// 2. Serialization/Deserialization
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(Singleton.getInstance());
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton deserializedSingleton = (Singleton) in.readObject();
// 3. Cloning (if Cloneable is implemented)
Singleton cloned = (Singleton) Singleton.getInstance().clone();
Q5: Alternatives to Singleton pattern?
A:
- Dependency Injection: Pass dependencies explicitly
- Factory Pattern: Control object creation
- Registry Pattern: Manage object instances
- Monostate Pattern: Share state, not instance
💡 Design Questions
Q6: Design a thread-safe Logger using Singleton pattern
A:
java
public class Logger {
private static volatile Logger instance;
private final PrintWriter writer;
private final Object lock = new Object();
private Logger() {
try {
writer = new PrintWriter(new FileWriter("app.log", true));
} catch (IOException e) {
throw new RuntimeException("Cannot initialize logger", e);
}
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public void log(String level, String message) {
String timestamp = LocalDateTime.now().toString();
String logEntry = String.format("[%s] %s: %s%n", timestamp, level, message);
synchronized (lock) {
writer.print(logEntry);
writer.flush();
}
}
// Prevent cloning
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Cannot clone singleton");
}
// Handle serialization
protected Object readResolve() {
return getInstance();
}
}
🎯 Summary
Aspect | Details |
---|---|
Intent | Ensure single instance with global access |
Problem | Need exactly one instance system-wide |
Solution | Private constructor + static getInstance() |
Benefits | Controlled access, reduced memory footprint |
Drawbacks | Testing difficulties, tight coupling |
Use When | Exactly one instance needed (DB, Logger, Config) |
📚 Key Takeaways
- Choose the right implementation based on your needs
- Consider enum-based approach for simplicity in Java
- Handle thread safety properly
- Be aware of testing challenges
- Consider alternatives like dependency injection