Home ยป Java Singleton Design Pattern Tutorial

Java Singleton Design Pattern Tutorial

A Singleton class in Java is a class that allows only one instance of itself to be created throughout the lifetime of the application.

The Singleton design pattern is useful when you need exactly one object to coordinate actions across the system, such as managing a database connection, logging, or controlling access to shared resources.

In this tutorial, we will explore different ways to implement a Singleton class in Java, with various code examples demonstrating the best practices.

Key Features of a Singleton Class:

Private Constructor: The constructor is made private to prevent direct instantiation from outside the class.
Static Instance: The class holds a static instance of itself.
Global Access: A public static method provides a global point of access to the instance.

1. Eager Initialization Singleton

In eager initialization, the instance of the Singleton class is created at the time of class loading. This is the simplest method to create a Singleton, but it may lead to resource wastage if the instance is never used.

Example:

public class EagerSingleton {
    // Static instance of the Singleton class, created eagerly
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // Private constructor to prevent instantiation
    private EagerSingleton() {
        System.out.println("EagerSingleton instance created");
    }

    // Public method to provide access to the instance
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        EagerSingleton singleton = EagerSingleton.getInstance();
    }
}

Explanation:

The INSTANCE is created at class loading time.
The getInstance() method provides global access to the Singleton instance.

Output:

EagerSingleton instance created (printed only once).

2. Lazy Initialization Singleton

In lazy initialization, the instance is created only when it is needed, i.e., when getInstance() is called for the first time. This can save resources if the instance is never used.

Example:

public class LazySingleton {
    // Static instance of the Singleton class, not initialized initially
    private static LazySingleton instance;

    // Private constructor to prevent instantiation
    private LazySingleton() {
        System.out.println("LazySingleton instance created");
    }

    // Public method to provide access to the instance, creating it lazily
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        LazySingleton singleton = LazySingleton.getInstance();
    }
}

Explanation:

The instance is created lazily when getInstance() is first called.
The instance is created only if instance == null.

Output:
LazySingleton instance created.

3. Thread-Safe Singleton (Synchronized)

To make the Singleton thread-safe, we can synchronize the getInstance() method so that only one thread can execute it at a time. This prevents race conditions in multithreaded environments.

Example:

public class ThreadSafeSingleton {
    // Static instance of the Singleton class, not initialized initially
    private static ThreadSafeSingleton instance;

    // Private constructor to prevent instantiation
    private ThreadSafeSingleton() {
        System.out.println("ThreadSafeSingleton instance created");
    }

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

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        ThreadSafeSingleton singleton = ThreadSafeSingleton.getInstance();
    }
}

Explanation:

The synchronized keyword ensures that only one thread can create the instance.
This prevents multiple threads from creating multiple instances.

Output:
ThreadSafeSingleton instance created.

4. Double-Checked Locking Singleton

The double-checked locking technique reduces the overhead of synchronization by only synchronizing the block of code that creates the instance, rather than the entire method. This is more efficient than synchronizing the entire method.

Example:

public class DoubleCheckedLockingSingleton {
    // Static instance of the Singleton class, using volatile to ensure visibility across threads
    private static volatile DoubleCheckedLockingSingleton instance;

    // Private constructor to prevent instantiation
    private DoubleCheckedLockingSingleton() {
        System.out.println("DoubleCheckedLockingSingleton instance created");
    }

    // Double-checked locking for thread-safe and efficient Singleton
    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        DoubleCheckedLockingSingleton singleton = DoubleCheckedLockingSingleton.getInstance();
    }
}

Explanation:

The volatile keyword ensures that the instance variable is read from the main memory, not from the thread's local cache.
The first if check prevents unnecessary synchronization, and the second if ensures that the instance is only created once.

Output:
DoubleCheckedLockingSingleton instance created.

5. Bill Pugh Singleton (Inner Static Helper Class)

This method is considered one of the best ways to implement a Singleton in Java. It takes advantage of Java's class loading mechanism to ensure that the Singleton is created only when needed, without the need for synchronization.

Example:

public class BillPughSingleton {
    // Private constructor to prevent instantiation
    private BillPughSingleton() {
        System.out.println("BillPughSingleton instance created");
    }

    // Inner static helper class responsible for holding the Singleton instance
    private static class SingletonHelper {
        // The Singleton instance is created when this class is loaded
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    // Public method to provide access to the Singleton instance
    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        BillPughSingleton singleton = BillPughSingleton.getInstance();
    }
}

Explanation:

The SingletonHelper class is not loaded until getInstance() is called, ensuring lazy initialization.
This approach is thread-safe without requiring synchronization.

Output:
BillPughSingleton instance created.

6. Enum Singleton

Using an enum to implement a Singleton is the most robust way, as it provides thread safety, serialization safety, and guarantees that only one instance will be created.

Example:

public enum EnumSingleton {
    INSTANCE;  // The single instance

    public void doSomething() {
        System.out.println("Enum Singleton is working!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        singleton.doSomething();
    }
}

Explanation:

The INSTANCE is the single instance of EnumSingleton.
Enum-based Singleton ensures that only one instance is created, even in multithreaded environments or when dealing with serialization.

Output:
Enum Singleton is working!.

7. Singleton with Reflection Safe

One of the drawbacks of using traditional Singleton methods is that reflection can break the Singleton pattern by allowing multiple instances to be created. To prevent this, you can add a check in the constructor.

Example:

public class ReflectionSafeSingleton {
    // Static instance of the Singleton class
    private static ReflectionSafeSingleton instance;

    // Private constructor to prevent instantiation
    private ReflectionSafeSingleton() {
        // Prevent reflection from breaking Singleton
        if (instance != null) {
            throw new RuntimeException("Cannot create instance using reflection");
        }
        System.out.println("ReflectionSafeSingleton instance created");
    }

    // Public method to provide access to the instance
    public static ReflectionSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ReflectionSafeSingleton();
        }
        return instance;
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        ReflectionSafeSingleton singleton = ReflectionSafeSingleton.getInstance();
    }
}

Explanation:

The constructor checks if an instance already exists, and if it does, it throws an exception to prevent reflection-based instantiation.

Output:
ReflectionSafeSingleton instance created.

8. Singleton with Serialization Protection

When a Singleton class implements Serializable, it can create new instances during deserialization. To prevent this, override the readResolve() method to return the same instance.

Example:

import java.io.Serializable;

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

    // Static instance of the Singleton class
    private static final SerializedSingleton INSTANCE = new SerializedSingleton();

    // Private constructor to prevent instantiation
    private SerializedSingleton() {
        System.out.println("SerializedSingleton instance created");
    }

    // Public method to provide access to the instance
    public static SerializedSingleton getInstance() {
        return INSTANCE;
    }

    // readResolve method to prevent creating a new instance during deserialization
    protected Object readResolve() {
        return getInstance();
    }
}

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        SerializedSingleton singleton = SerializedSingleton.getInstance();
    }
}

Explanation:

The readResolve() method ensures that the same instance is returned during deserialization.

Output:
SerializedSingleton instance created.

Conclusion

The Singleton design pattern ensures that only one instance of a class is created and provides global access to that instance. In Java, there are several ways to implement the Singleton pattern:

Eager Initialization: The instance is created when the class is loaded.
Lazy Initialization: The instance is created when it is first accessed.
Thread-Safe Singleton: The synchronized keyword ensures only one thread creates the instance.
Double-Checked Locking: Minimizes the cost of synchronization with a double check.
Bill Pugh Singleton: Inner static class provides a lazy-loaded thread-safe Singleton.
Enum Singleton: The most robust and straightforward implementation.
Reflection and Serialization Safe: Methods to protect against breaking the Singleton pattern with reflection or serialization.

By choosing the appropriate method for your application's needs, you can implement Singleton classes effectively in Java.

You may also like