Singleton Pattern in Java

Introduction

Imagine you have a settings manager in your application. You only need one instance of this manager throughout your entire program because all parts of your app should use the same settings. If you create multiple instances, they might have different configurations and cause chaos!

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance. It's like having only one captain on a ship—everyone takes directions from the same person.


What is the Singleton Pattern?

The Singleton Pattern is a creational design pattern that restricts the instantiation of a class to a single object. It's used when exactly one object is needed to coordinate actions across the system.

Key Characteristics:

  • Only one instance throughout application lifetime
  • Global access point to that instance
  • Controlled instantiation - prevents external creation
  • Lazy initialization - created only when needed

Code Explanation with Examples

Example 1: Basic Singleton (Eager Initialization)

public class BasicSingleton {
// 1. Private static instance - created when class loads
private static final BasicSingleton instance = new BasicSingleton();
// 2. Private constructor - prevents external instantiation
private BasicSingleton() {
System.out.println("BasicSingleton instance created!");
}
// 3. Public static method to get the instance
public static BasicSingleton getInstance() {
return instance;
}
// Business method
public void showMessage() {
System.out.println("Hello from BasicSingleton!");
}
}
// Usage
public class SingletonDemo {
public static void main(String[] args) {
// Get the singleton instance
BasicSingleton singleton1 = BasicSingleton.getInstance();
BasicSingleton singleton2 = BasicSingleton.getInstance();
singleton1.showMessage();
// Check if both references point to same object
System.out.println("Same instance? " + (singleton1 == singleton2)); // true
System.out.println("singleton1 hash: " + singleton1.hashCode());
System.out.println("singleton2 hash: " + singleton2.hashCode());
}
}

Output:

BasicSingleton instance created!
Hello from BasicSingleton!
Same instance? true
singleton1 hash: 1324119927
singleton2 hash: 1324119927

Example 2: Lazy Initialization Singleton

public class LazySingleton {
// 1. Private static instance - not created until needed
private static LazySingleton instance;
// 2. Private constructor
private LazySingleton() {
System.out.println("LazySingleton instance created!");
}
// 3. Public static method with lazy initialization
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void doWork() {
System.out.println("LazySingleton is working...");
}
}
// Usage
public class LazyDemo {
public static void main(String[] args) {
System.out.println("Application started");
// Instance created only when first requested
LazySingleton singleton = LazySingleton.getInstance();
singleton.doWork();
// Subsequent calls return the same instance
LazySingleton anotherReference = LazySingleton.getInstance();
System.out.println("Same instance? " + (singleton == anotherReference));
}
}

Output:

Application started
LazySingleton instance created!
LazySingleton is working...
Same instance? true

Example 3: Thread-Safe Singleton (Synchronized Method)

public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
System.out.println("ThreadSafeSingleton instance created!");
}
// Synchronized method for thread safety
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
public void processData(String data) {
System.out.println("Processing: " + data);
}
}
// Usage with multiple threads
public class ThreadSafeDemo {
public static void main(String[] args) {
Runnable task = () -> {
ThreadSafeSingleton singleton = ThreadSafeSingleton.getInstance();
singleton.processData(Thread.currentThread().getName());
};
// Create multiple threads
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
Thread thread3 = new Thread(task, "Thread-3");
thread1.start();
thread2.start();
thread3.start();
}
}

Example 4: Double-Checked Locking (Most Efficient)

public class DoubleCheckedSingleton {
// volatile ensures multiple threads handle instance correctly
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {
System.out.println("DoubleCheckedSingleton instance created!");
}
public static DoubleCheckedSingleton getInstance() {
// First check (without synchronization)
if (instance == null) {
// Synchronize only when instance is null
synchronized (DoubleCheckedSingleton.class) {
// Second check (with synchronization)
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
public void performTask() {
System.out.println("Task performed by DoubleCheckedSingleton");
}
}

Example 5: Bill Pugh Singleton (Initialization-on-demand Holder)

public class BillPughSingleton {
// Private constructor
private BillPughSingleton() {
System.out.println("BillPughSingleton instance created!");
}
// Static inner helper class - loaded only when getInstance() is called
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
public void serve() {
System.out.println("BillPughSingleton serving your request!");
}
}
// Usage
public class BillPughDemo {
public static void main(String[] args) {
BillPughSingleton singleton = BillPughSingleton.getInstance();
singleton.serve();
}
}

Example 6: Real-World Configuration Manager

import java.util.*;
// Real-world example: Application Configuration Manager
public class ConfigurationManager {
private static ConfigurationManager instance;
private Properties properties;
private ConfigurationManager() {
loadConfiguration();
System.out.println("ConfigurationManager initialized");
}
public static synchronized ConfigurationManager getInstance() {
if (instance == null) {
instance = new ConfigurationManager();
}
return instance;
}
private void loadConfiguration() {
properties = new Properties();
// In real application, load from file/database
properties.setProperty("database.url", "jdbc:mysql://localhost:3306/mydb");
properties.setProperty("database.username", "admin");
properties.setProperty("database.password", "secret");
properties.setProperty("app.version", "1.0.0");
properties.setProperty("max.connections", "10");
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public void setProperty(String key, String value) {
properties.setProperty(key, value);
}
public void displayConfig() {
System.out.println("=== Application Configuration ===");
properties.forEach((key, value) -> 
System.out.println(key + " = " + value));
}
}
// Usage throughout application
public class App {
public static void main(String[] args) {
// Different parts of application access same config
ConfigurationManager config1 = ConfigurationManager.getInstance();
ConfigurationManager config2 = ConfigurationManager.getInstance();
System.out.println("Same config instance? " + (config1 == config2));
// Use configuration
System.out.println("Database URL: " + config1.getProperty("database.url"));
System.out.println("App Version: " + config1.getProperty("app.version"));
config1.displayConfig();
}
}

Example 7: Logger Singleton

import java.io.*;
import java.time.LocalDateTime;
// Real-world example: Logger
public class Logger {
private static Logger instance;
private PrintWriter writer;
private Logger() {
try {
// Open log file
writer = new PrintWriter(new FileWriter("application.log", true));
System.out.println("Logger initialized");
} catch (IOException e) {
e.printStackTrace();
}
}
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String level, String message) {
String logEntry = String.format("[%s] %s: %s", 
LocalDateTime.now(), level, message);
writer.println(logEntry);
writer.flush(); // Ensure message is written immediately
}
public void info(String message) {
log("INFO", message);
}
public void error(String message) {
log("ERROR", message);
}
public void warn(String message) {
log("WARN", message);
}
public void close() {
if (writer != null) {
writer.close();
}
}
}
// Usage across different classes
public class UserService {
private Logger logger = Logger.getInstance();
public void createUser(String username) {
logger.info("Creating user: " + username);
// Business logic...
logger.info("User created successfully: " + username);
}
}
public class OrderService {
private Logger logger = Logger.getInstance();
public void processOrder(String orderId) {
logger.info("Processing order: " + orderId);
// Business logic...
logger.warn("Order processing taking longer than expected: " + orderId);
}
}
// Demo
public class LoggerDemo {
public static void main(String[] args) {
UserService userService = new UserService();
OrderService orderService = new OrderService();
userService.createUser("john_doe");
orderService.processOrder("ORD-12345");
// All log messages go to the same file through same Logger instance
}
}

Example 8: Enum Singleton (Best Practice)

// Using Enum is the simplest and safest way to implement Singleton
public enum EnumSingleton {
INSTANCE; // The single instance
// Instance variables
private int value;
private String name;
// Methods
public void doSomething() {
System.out.println("EnumSingleton doing something with value: " + value);
}
// Getters and setters
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// Usage
public class EnumSingletonDemo {
public static void main(String[] args) {
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
EnumSingleton singleton2 = EnumSingleton.INSTANCE;
singleton1.setValue(42);
singleton1.setName("MySingleton");
singleton2.doSomething(); // Uses the same instance!
System.out.println("Same instance? " + (singleton1 == singleton2));
System.out.println("Value from singleton2: " + singleton2.getValue());
}
}

Singleton Implementation Comparison

MethodProsConsUse When
EagerSimple, thread-safeInstance created even if not usedResource is always needed
LazyCreates only when neededNot thread-safeSingle-threaded environment
SynchronizedThread-safePerformance overheadMulti-threaded but low concurrency
Double-CheckedBest performance, thread-safeMore complexHigh-performance multi-threaded apps
Bill PughSimple, thread-safe, efficient-Most general cases
EnumSimplest, thread-safe, serialization-safe-Best practice for all cases

When to Use Singleton Pattern

✅ Appropriate Uses:

  • Configuration Management - Single source of truth for settings
  • Logging - All components write to same log file
  • Database Connection Pool - Manage limited database connections
  • Caching - Application-level cache
  • Service Locators - Central point for finding services

❌ Avoid When:

  • Testing - Singletons can make unit testing difficult
  • Global State - Can lead to hidden dependencies
  • Multiple Instances Needed - If you might need multiple instances later

Best Practices

  1. Use Enum for Simplicity: EnumSingleton.INSTANCE is the easiest and safest
  2. Consider Dependency Injection: For better testability
  3. Make it Thread-Safe: In multi-threaded environments
  4. Prevent Reflection Attacks: Use enum or check in constructor
  5. Handle Serialization: Implement readResolve() if using serialization

Common Pitfalls

// ❌ DON'T: Public static field (no control)
public class BadSingleton {
public static final BadSingleton INSTANCE = new BadSingleton();
private BadSingleton() {}
}
// ❌ DON'T: Not thread-safe in multi-threaded environment
public class UnsafeSingleton {
private static UnsafeSingleton instance;
private UnsafeSingleton() {}
public static UnsafeSingleton getInstance() {
if (instance == null) { // Race condition!
instance = new UnsafeSingleton();
}
return instance;
}
}

Conclusion

The Singleton Pattern is like having only one manager for a specific job in your application:

  • Guarantees single instance - no duplicates causing conflicts
  • Global access point - easy to reach from anywhere
  • Controlled initialization - created when and how you want
  • Memory efficient - only one instance in memory

Key Takeaway: Use the Enum approach for simplicity and safety, or Bill Pugh's method for lazy initialization. Remember that while Singleton solves certain problems, it should be used judiciously as it introduces global state into your application.

It's perfect for when you truly need exactly one instance of a class to coordinate actions across your system!

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper