Singleton Design Pattern in Java

 

1. What is Singleton Design Pattern?

Definition

Singleton is a creational design pattern that ensures a class has only one instance throughout the application lifecycle and provides a global access point to that instance.

Key Characteristics

  • Single Instance: Only one object of the class exists in the JVM
  • Global Access: Provides a way for external code to access this instance
  • Self-Instantiation: The class itself is responsible for creating its instance
  • Controlled Access: Prevents external instantiation through private constructor

2. Why Use Singleton Pattern?

Problem Statement

Imagine you have a database connection manager. Creating multiple connection managers can lead to:

  • Resource wastage (multiple connections open)
  • Inconsistent state across the application
  • Configuration conflicts
  • Memory overhead

Solution

Singleton ensures that only one connection manager exists, shared across the entire application.

When to Use Singleton

✅ Use When:

  • Managing shared resources (database connections, file systems)
  • Configuration management (one configuration object)
  • Logging mechanism (centralized logging)
  • Caching (single cache instance)
  • Thread pools
  • Device drivers

❌ Avoid When:

  • You need multiple instances with different states
  • Testing requires isolation between test cases
  • The pattern introduces unnecessary global state

3. How Does Singleton Work?

Core Components

Three Essential Elements:

  1. Private Constructor

    • Prevents external instantiation using new keyword
    • Forces use of the static factory method
  2. Private Static Instance

    • Holds the single instance of the class
    • Static ensures it belongs to the class, not individual objects
  3. Public Static Method (getInstance)

    • Provides global access to the instance
    • Creates the instance if it doesn’t exist
    • Returns the existing instance if already created

Basic Flow Diagram

Client Request → getInstance() → Check if instance exists
                                        ↓
                                   YES ←→ NO
                                    ↓       ↓
                            Return existing  Create new
                                instance      instance
                                    ↓           ↓
                                    ←-----------
                                        ↓
                                  Return instance

4. Implementation Methods

Method 1: Eager Initialization

What: Instance is created at class loading time.

Code:

public class EagerSingleton {
    // Instance created at class loading
    private static final EagerSingleton instance = new EagerSingleton();

    // Private constructor
    private EagerSingleton() {
        System.out.println("Eager Singleton Created!");
    }

    // Global access point
    public static EagerSingleton getInstance() {
        return instance;
    }

    public void showMessage() {
        System.out.println("Hello from Eager Singleton");
    }
}

How it Works:

  • JVM creates the instance when the class is loaded
  • Instance is created even if never used
  • Thread-safe by default (class loading is thread-safe)

Pros:

  • Simple and straightforward
  • Thread-safe without synchronization
  • No null checks needed

Cons:

  • Instance created even if not needed (memory waste)
  • No exception handling during creation
  • Not suitable for resource-intensive objects

When to Use:

  • Application always needs this singleton
  • Object creation is lightweight
  • No exception handling needed

Method 2: Lazy Initialization (Not Thread-Safe)

What: Instance is created only when first requested.

Code:

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
        System.out.println("Lazy Singleton Created!");
    }

    public static LazySingleton getInstance() {
        // Create only when needed
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

How it Works:

  • Instance is null initially
  • First call to getInstance() creates the instance
  • Subsequent calls return the existing instance

The Problem:

// Thread 1                     // Thread 2
getInstance() {                 getInstance() {
    if (instance == null) {         if (instance == null) {
        // Both threads here!
        instance = new ...              instance = new ...
    }                               }
}                               }
// Result: Two instances created! ❌

Pros:

  • Memory efficient (created when needed)
  • Simple implementation

Cons:

  • NOT thread-safe
  • Multiple threads can create multiple instances
  • Breaks singleton contract in multithreading

When to Use:

  • Single-threaded applications only
  • For learning purposes

Method 3: Thread-Safe Singleton (Synchronized Method)

What: Makes getInstance() thread-safe using synchronization.

Code:

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {
        System.out.println("Thread-Safe Singleton Created!");
    }

    // Synchronized method
    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

How it Works:

  • synchronized keyword locks the method
  • Only one thread can execute getInstance() at a time
  • Other threads wait until the lock is released

Pros:

  • Thread-safe
  • Lazy initialization
  • Simple to implement

Cons:

  • Performance overhead - synchronization on every call
  • Unnecessary locking after instance is created
  • Slow in high-concurrency scenarios

When to Use:

  • Multithreaded environments
  • Performance is not critical
  • getInstance() not called frequently

Method 4: Double-Checked Locking (DCL)

What: Optimized thread-safe implementation with minimal locking.

Code:

public class DoubleCheckedLockingSingleton {
    // volatile ensures visibility across threads
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {
        System.out.println("DCL Singleton Created!");
    }

    public static DoubleCheckedLockingSingleton getInstance() {
        // First check (no locking)
        if (instance == null) {
            // Synchronize only for creation
            synchronized (DoubleCheckedLockingSingleton.class) {
                // Second check (with locking)
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

How it Works:

  1. First Check: If instance exists, return immediately (no locking)
  2. Synchronization: Only enter synchronized block if instance is null
  3. Second Check: Inside sync block, check again (another thread might have created it)
  4. Creation: Create instance only if still null

Why volatile?

  • Prevents instruction reordering by JVM
  • Ensures all threads see the fully constructed instance
  • Without volatile, threads may see partially constructed objects

Visualization:

Thread 1: Check null → Lock → Check null again → Create
Thread 2: Check null → Wait for lock → Check null (now false) → Return existing
Thread 3: Check null (now false) → Return existing (no locking!)

Pros:

  • Thread-safe
  • Lazy initialization
  • Minimal synchronization overhead
  • High performance

Cons:

  • Complex implementation
  • Requires Java 5+ for proper volatile support
  • Still some overhead for first few threads

When to Use:

  • High-concurrency applications
  • Performance is critical
  • Resource-intensive singleton creation

Method 5: Bill Pugh Singleton (Static Inner Class)

What: Uses static inner class for lazy initialization without synchronization.

Code:

public class BillPughSingleton {

    private BillPughSingleton() {
        System.out.println("Bill Pugh Singleton Created!");
    }

    // Static inner class - not loaded until referenced
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

How it Works:

  • Inner class SingletonHelper is not loaded when outer class loads
  • Inner class loads only when getInstance() is called first time
  • JVM guarantees thread-safe class loading
  • No synchronization needed

Why it’s Clever:

// When BillPughSingleton class loads:
// - Outer class loaded
// - SingletonHelper NOT loaded yet ✓

// When getInstance() called first time:
// - SingletonHelper class loaded
// - INSTANCE created (thread-safe by JVM)
// - INSTANCE returned

// Subsequent calls:
// - Return already created INSTANCE
// - No synchronization overhead ✓

Pros:

  • Lazy initialization
  • Thread-safe without synchronization
  • High performance
  • Clean and elegant code
  • Best approach for most cases

Cons:

  • Cannot pass parameters to constructor
  • Slightly more complex conceptually

When to Use:

  • Most production applications
  • When you need lazy initialization
  • When performance matters

Method 6: Enum Singleton (Best Practice)

What: Uses Java enum to implement singleton.

Code:

public enum EnumSingleton {
    INSTANCE; // The singleton instance

    // Constructor (called only once)
    EnumSingleton() {
        System.out.println("Enum Singleton Created!");
    }

    // Your methods
    public void showMessage() {
        System.out.println("Hello from Enum Singleton");
    }

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

// Usage
EnumSingleton.INSTANCE.showMessage();

How it Works:

  • JVM guarantees only one instance of each enum constant
  • Inherently thread-safe
  • Immune to reflection and serialization attacks

Why Enums are Special:

// Regular class - can be broken
Constructor<?> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance = constructor.newInstance(); // Breaks singleton! ❌

// Enum - reflection cannot create instance
// Java throws IllegalArgumentException ✓

Pros:

  • Simplest implementation
  • Best protection against reflection
  • Serialization-safe by default
  • Thread-safe without effort
  • Recommended by Joshua Bloch (Effective Java)

Cons:

  • Cannot extend other classes (enums already extend Enum)
  • Cannot use lazy initialization
  • May seem unconventional to beginners

When to Use:

  • Most new projects
  • When security is important
  • When serialization is needed
  • Default choice unless you have specific reasons

Method 7: Static Block Initialization

What: Similar to eager initialization but allows exception handling.

Code:

public class StaticBlockSingleton {
    private static StaticBlockSingleton instance;

    private StaticBlockSingleton() {
        System.out.println("Static Block Singleton Created!");
    }

    // Static block for initialization
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception during singleton creation", e);
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

How it Works:

  • Static block executes when class loads
  • Allows try-catch for exception handling
  • Instance created eagerly like eager initialization

Pros:

  • Can handle exceptions during creation
  • Thread-safe

Cons:

  • Eager initialization (same as Method 1)
  • More verbose than simple eager initialization

When to Use:

  • Need exception handling during creation
  • Reading configuration files during initialization

5. Pros and Cons of Singleton Pattern

Advantages ✅

1. Controlled Access to Single Instance

DatabaseConnection conn1 = DatabaseConnection.getInstance();
DatabaseConnection conn2 = DatabaseConnection.getInstance();
// conn1 == conn2 (same object)

2. Reduced Memory Footprint

  • Only one instance in memory
  • Shared resource across application

3. Global Access Point

// From anywhere in application
Logger.getInstance().log("Application started");

4. Lazy Initialization (Some Implementations)

  • Created only when needed
  • Saves resources if never used

5. Thread-Safe Access (When Implemented Correctly)

  • Prevents race conditions
  • Ensures consistency

Disadvantages ❌

1. Violates Single Responsibility Principle

  • Class manages both its functionality AND its instantiation
  • Two reasons to change

2. Global State

// Hidden dependencies
class UserService {
    public void createUser() {
        // Hidden dependency on Logger
        Logger.getInstance().log("Creating user");
    }
}

3. Difficult to Test

// Test 1
Logger.getInstance().log("Test 1");

// Test 2 - uses same logger instance
// Cannot reset state between tests
Logger.getInstance().log("Test 2");

4. Concurrency Issues (If Poorly Implemented)

  • Thread-safety requires careful implementation
  • Performance vs. correctness tradeoffs

5. Hidden Dependencies

  • Makes code harder to understand
  • Dependencies not visible in constructor

6. Tight Coupling

// Tightly coupled to singleton
class PaymentService {
    public void processPayment() {
        DatabaseConnection.getInstance().save(payment);
    }
}

Better Approach (Dependency Injection):

class PaymentService {
    private DatabaseConnection connection;

    // Dependencies explicit and testable
    public PaymentService(DatabaseConnection connection) {
        this.connection = connection;
    }
}

6. Breaking the Singleton Pattern

Why Would You Want to Break It?

  • To test different scenarios
  • To understand its vulnerabilities
  • To implement proper security

Method 1: Reflection Attack

How Reflection Breaks Singleton:

public class ReflectionAttack {
    public static void main(String[] args) {
        try {
            // Get the singleton instance normally
            LazySingleton instance1 = LazySingleton.getInstance();

            // Use reflection to access private constructor
            Constructor<LazySingleton> constructor =
                LazySingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true); // Bypass private

            // Create second instance - breaks singleton!
            LazySingleton instance2 = constructor.newInstance();

            System.out.println("Instance 1 hash: " + instance1.hashCode());
            System.out.println("Instance 2 hash: " + instance2.hashCode());
            System.out.println("Are they same? " + (instance1 == instance2));
            // Output: false ❌ Singleton broken!

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

How to Prevent Reflection Attack:

public class ReflectionSafeSingleton {
    private static ReflectionSafeSingleton instance;

    private ReflectionSafeSingleton() {
        // Prevent reflection attack
        if (instance != null) {
            throw new RuntimeException("Use getInstance() method");
        }
    }

    public static ReflectionSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ReflectionSafeSingleton();
        }
        return instance;
    }
}

Best Defense - Use Enum:

// Enum is immune to reflection
public enum EnumSingleton {
    INSTANCE;
}

// Reflection throws IllegalArgumentException
// Cannot reflectively create enum objects ✓

Method 2: Serialization/Deserialization Attack

How Serialization Breaks Singleton:

public class SerializationAttack {
    public static void main(String[] args) {
        try {
            LazySingleton instance1 = LazySingleton.getInstance();

            // Serialize the singleton
            ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("singleton.ser")
            );
            out.writeObject(instance1);
            out.close();

            // Deserialize - creates NEW instance!
            ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("singleton.ser")
            );
            LazySingleton instance2 = (LazySingleton) in.readObject();
            in.close();

            System.out.println("Instance 1 hash: " + instance1.hashCode());
            System.out.println("Instance 2 hash: " + instance2.hashCode());
            System.out.println("Are they same? " + (instance1 == instance2));
            // Output: false ❌ Singleton broken!

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

How to Prevent Serialization Attack:

import java.io.Serializable;

public class SerializationSafeSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static SerializationSafeSingleton instance;

    private SerializationSafeSingleton() {
        if (instance != null) {
            throw new RuntimeException("Use getInstance()");
        }
    }

    public static SerializationSafeSingleton getInstance() {
        if (instance == null) {
            instance = new SerializationSafeSingleton();
        }
        return instance;
    }

    // This method prevents deserialization from creating new instance
    protected Object readResolve() {
        return getInstance();
    }
}

What readResolve() Does:

  • Called automatically during deserialization
  • Returns the existing singleton instance
  • Prevents creation of new instance

Best Defense - Use Enum:

// Enum handles serialization automatically
public enum EnumSingleton implements Serializable {
    INSTANCE;
}
// Deserialization always returns same instance ✓

Method 3: Cloning Attack

How Cloning Breaks Singleton:

public class CloneAttack {
    public static void main(String[] args) {
        try {
            LazySingleton instance1 = LazySingleton.getInstance();

            // Clone the singleton
            LazySingleton instance2 = (LazySingleton) instance1.clone();

            System.out.println("Instance 1 hash: " + instance1.hashCode());
            System.out.println("Instance 2 hash: " + instance2.hashCode());
            System.out.println("Are they same? " + (instance1 == instance2));
            // Output: false ❌ Singleton broken!

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

How to Prevent Cloning Attack:

public class CloneSafeSingleton implements Cloneable {
    private static CloneSafeSingleton instance;

    private CloneSafeSingleton() {}

    public static CloneSafeSingleton getInstance() {
        if (instance == null) {
            instance = new CloneSafeSingleton();
        }
        return instance;
    }

    // Prevent cloning
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Cloning not allowed");
    }
}

Or Don’t Implement Cloneable:

// Simply don't implement Cloneable interface
public class Singleton {
    // No Cloneable = No clone() = Safe ✓
}

Method 4: Multiple Classloaders

How Multiple Classloaders Break Singleton:

public class ClassLoaderAttack {
    public static void main(String[] args) {
        try {
            // Load class with two different classloaders
            ClassLoader loader1 = new URLClassLoader(urls);
            ClassLoader loader2 = new URLClassLoader(urls);

            Class<?> class1 = loader1.loadClass("LazySingleton");
            Class<?> class2 = loader2.loadClass("LazySingleton");

            Object instance1 = class1.getMethod("getInstance").invoke(null);
            Object instance2 = class2.getMethod("getInstance").invoke(null);

            System.out.println("Are they same? " + (instance1 == instance2));
            // Output: false ❌ Different instances!

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Prevention:

  • Difficult to prevent completely
  • Use application server’s classloader hierarchy properly
  • Consider using Spring’s dependency injection instead

7. Best Practices

Choose the Right Implementation

Quick Decision Guide:

Need serialization?
    → Use Enum Singleton

Need lazy initialization + high performance?
    → Use Bill Pugh (Static Inner Class)

Simple case + always needed?
    → Use Eager Initialization

Learning/teaching?
    → Start with Lazy, show why it fails

Need constructor parameters?
    → Consider if you really need Singleton
      (might need Factory pattern instead)

Protect Against Attacks

Complete Attack-Resistant Singleton:

public enum SecureSingleton {
    INSTANCE;

    // Your fields
    private int counter = 0;

    // Constructor (optional)
    SecureSingleton() {
        System.out.println("Singleton initialized");
    }

    // Your methods
    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }
}

// Usage
SecureSingleton.INSTANCE.increment();
System.out.println(SecureSingleton.INSTANCE.getCounter());

Protection Summary:

  • ✅ Thread-safe (by JVM)
  • ✅ Reflection-safe (enums cannot be reflectively instantiated)
  • ✅ Serialization-safe (JVM handles it)
  • ✅ Clone-safe (enums are not Cloneable)

Testing Considerations

Problem with Singletons in Tests:

public class UserServiceTest {
    @Test
    public void test1() {
        Logger.getInstance().setLevel("DEBUG");
        // Test code
    }

    @Test
    public void test2() {
        // Still in DEBUG mode from test1! ❌
        // Tests are not isolated
    }
}

Better Approach - Dependency Injection:

public class UserService {
    private final Logger logger;

    // Inject dependency
    public UserService(Logger logger) {
        this.logger = logger;
    }
}

// In tests
@Test
public void test1() {
    Logger mockLogger = mock(Logger.class);
    UserService service = new UserService(mockLogger);
    // Test with mock ✓
}

When NOT to Use Singleton

❌ Avoid Singleton When:

  1. Objects need different states:
// BAD - needs multiple configurations
ConfigurationSingleton.getInstance().setEnvironment("dev");
ConfigurationSingleton.getInstance().setEnvironment("prod"); // Conflict!

// GOOD - multiple instances
Configuration devConfig = new Configuration("dev");
Configuration prodConfig = new Configuration("prod");
  1. You need testability:
// BAD - hard to test
public class PaymentProcessor {
    public void process() {
        DatabaseSingleton.getInstance().save();
    }
}

// GOOD - easy to test with mocks
public class PaymentProcessor {
    private Database database;

    public PaymentProcessor(Database database) {
        this.database = database;
    }
}
  1. Inheritance is needed:
// Singleton with private constructor cannot be extended
// Use normal classes with dependency injection instead

8. Real-World Examples

Example 1: Logger Class

Complete Implementation:

public enum Logger {
    INSTANCE;

    private String logLevel = "INFO";

    public void log(String message) {
        System.out.println("[" + logLevel + "] " +
            LocalDateTime.now() + " - " + message);
    }

    public void setLogLevel(String level) {
        this.logLevel = level;
    }
}

// Usage across application
public class UserService {
    public void createUser(String username) {
        Logger.INSTANCE.log("Creating user: " + username);
        // Create user logic
        Logger.INSTANCE.log("User created successfully");
    }
}

public class OrderService {
    public void createOrder(int orderId) {
        Logger.INSTANCE.log("Creating order: " + orderId);
        // Create order logic
        Logger.INSTANCE.log("Order created successfully");
    }
}

Example 2: Database Connection Manager

public class DatabaseConnection {
    private static volatile DatabaseConnection instance;
    private Connection connection;

    private DatabaseConnection() {
        try {
            // Initialize database connection
            String url = "jdbc:mysql://localhost:3306/mydb";
            String user = "root";
            String password = "password";
            this.connection = DriverManager.getConnection(url, user, password);
            System.out.println("Database connected!");
        } catch (SQLException e) {
            throw new RuntimeException("Failed to connect to database", e);
        }
    }

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }

    public void executeQuery(String sql) {
        try {
            Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            // Process results
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

// Usage
DatabaseConnection.getInstance().executeQuery("SELECT * FROM users");

Example 3: Configuration Manager

public enum ConfigurationManager {
    INSTANCE;

    private Properties properties;

    ConfigurationManager() {
        properties = new Properties();
        loadConfiguration();
    }

    private void loadConfiguration() {
        try {
            FileInputStream fis = new FileInputStream("config.properties");
            properties.load(fis);
            fis.close();
            System.out.println("Configuration loaded");
        } catch (IOException e) {
            System.err.println("Failed to load configuration");
        }
    }

    public String getProperty(String key) {
        return properties.getProperty(key);
    }

    public void setProperty(String key, String value) {
        properties.setProperty(key, value);
    }
}

// Usage
String dbUrl = ConfigurationManager.INSTANCE.getProperty("database.url");
String apiKey = ConfigurationManager.INSTANCE.getProperty("api.key");

Example 4: Cache Manager

public class CacheManager {
    private static CacheManager instance;
    private Map<String, Object> cache;

    private CacheManager() {
        cache = new ConcurrentHashMap<>();
        System.out.println("Cache initialized");
    }

    public static synchronized CacheManager getInstance() {
        if (instance == null) {
            instance = new CacheManager();
        }
        return instance;
    }

    public void put(String key, Object value) {
        cache.put(key, value);
        System.out.println("Cached: " + key);
    }

    public Object get(String key) {
        return cache.get(key);
    }

    public void clear() {
        cache.clear();
        System.out.println("Cache cleared");
    }

    public int size() {
        return cache.size();
    }
}

// Usage
CacheManager cache = CacheManager.getInstance();
cache.put("user:123", new User("John", "john@email.com"));
User user = (User) cache.get("user:123");

Comparison Table: All Implementation Methods

MethodThread-SafeLazy InitPerformanceComplexityRecommended
Eager Initialization⭐⭐⭐⭐⭐For lightweight objects
Lazy (Unsafe)⭐⭐⭐⭐⭐Never in production
Synchronized Method⭐⭐⭐⭐Low-concurrency apps
Double-Checked Lock⭐⭐⭐⭐⭐⭐⭐⭐High-performance needs
Bill Pugh⭐⭐⭐⭐⭐⭐⭐⭐Most cases
Enum⭐⭐⭐⭐⭐⭐⭐Best practice
Static Block⭐⭐⭐⭐⭐⭐⭐Need exception handling

Complete Working Example

Full application demonstrating different singleton patterns:

// Main test class
public class SingletonDemo {
    public static void main(String[] args) {
        System.out.println("=== Testing Different Singleton Patterns ===\n");

        // 1. Eager Singleton
        testEagerSingleton();

        // 2. Lazy Singleton
        testLazySingleton();

        // 3. Thread-Safe Singleton
        testThreadSafeSingleton();

        // 4. Bill Pugh Singleton
        testBillPughSingleton();

        // 5. Enum Singleton (Best Practice)
        testEnumSingleton();

        // 6. Breaking Singleton with Reflection
        testReflectionAttack();
    }

    static void testEagerSingleton() {
        System.out.println("--- Eager Singleton ---");
        EagerSingleton s1 = EagerSingleton.getInstance();
        EagerSingleton s2 = EagerSingleton.getInstance();
        System.out.println("Same instance? " + (s1 == s2));
        System.out.println();
    }

    static void testLazySingleton() {
        System.out.println("--- Lazy Singleton ---");
        LazySingleton s1 = LazySingleton.getInstance();
        LazySingleton s2 = LazySingleton.getInstance();
        System.out.println("Same instance? " + (s1 == s2));
        System.out.println();
    }

    static void testThreadSafeSingleton() {
        System.out.println("--- Thread-Safe Singleton ---");

        // Create multiple threads
        Thread t1 = new Thread(() -> {
            ThreadSafeSingleton s = ThreadSafeSingleton.getInstance();
            System.out.println("Thread 1: " + s.hashCode());
        });

        Thread t2 = new Thread(() -> {
            ThreadSafeSingleton s = ThreadSafeSingleton.getInstance();
            System.out.println("Thread 2: " + s.hashCode());
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();
    }

    static void testBillPughSingleton() {
        System.out.println("--- Bill Pugh Singleton ---");
        BillPughSingleton s1 = BillPughSingleton.getInstance();
        BillPughSingleton s2 = BillPughSingleton.getInstance();
        System.out.println("Same instance? " + (s1 == s2));
        System.out.println();
    }

    static void testEnumSingleton() {
        System.out.println("--- Enum Singleton (Best Practice) ---");
        EnumSingleton.INSTANCE.showMessage();
        EnumSingleton.INSTANCE.showMessage();
        System.out.println("Enum ensures single instance by JVM");
        System.out.println();
    }

    static void testReflectionAttack() {
        System.out.println("--- Reflection Attack Test ---");
        try {
            LazySingleton s1 = LazySingleton.getInstance();

            Constructor<LazySingleton> constructor =
                LazySingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            LazySingleton s2 = constructor.newInstance();

            System.out.println("Instance 1: " + s1.hashCode());
            System.out.println("Instance 2: " + s2.hashCode());
            System.out.println("Singleton broken by reflection? " + (s1 != s2));
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
    }
}

Golden Rules:

  • ✅ Use Enum Singleton when possible
  • ✅ Use Bill Pugh for lazy initialization
  • ✅ Always think about thread safety
  • ✅ Consider dependency injection as alternative
  • ❌ Never use lazy singleton without thread safety
  • ❌ Don’t use Singleton just to avoid passing parameters

Conclusion

The Singleton pattern is powerful but should be used judiciously. Modern Java development often favors dependency injection over singletons for better testability and maintainability. However, understanding singleton is crucial for:

  • Legacy code maintenance
  • Framework internals understanding
  • Interview preparation
  • Specific use cases like loggers and caches

Final Recommendation: Use Enum Singleton unless you have specific requirements that prevent it. It’s the simplest, safest, and most recommended approach by Java experts.

Post a Comment

Previous Post Next Post