Skip to content

Singleton Design Pattern

The Singleton Pattern is a frequently used design pattern in software development. It ensures that a class has only one instance and provides a global point of access to it. For example, for a database connection class, only one instance is needed to manage connections, and additional instances would be unnecessary.

  • Singleton is a design pattern which ensures that one instance of the singleton class exist at a time.
  • Two ways to create a singleton class:
  • Eager initialization
    • The instance is initialized at the time of creation.
  • Lazy initialization
    • The instance is initialized when the getInstance() method is called.

1 Eager initialization

Here’s a simple Singleton implementation using eager initialization:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();  // Create the instance eagerly

    private Singleton() {}   // Private constructor to prevent direct instantiation

    public static Singleton getInstance() {  // Provides access to the single instance
        return INSTANCE;
    }
}

Now, when we need to retrieve the instance, we use getInstance():

public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
}

This approach is called eager initialization since the instance is created when the class is loaded. Another approach is lazy initialization, where the instance is only created when needed.

// This is a singleton class
// the initialization of instance is eager
// the instance is created when the class is loaded
// the instance is shared by all threads
public class Singleton_EagerInitialization {
    private static Singleton_EagerInitialization instance = new Singleton_EagerInitialization();

    private Singleton_EagerInitialization() {
    }
    // private constructor, no one can create an instance of this class
    // the instance is created when the class is loaded



    public static Singleton_EagerInitialization getInstance(){
        return instance;
    }

    // static method can be called by class name
    // static belongs to class, not to object

    public static void main(String[] args){
        new Thread(() -> System.out.println(Singleton_EagerInitialization.getInstance())).start();
        new Thread(() -> System.out.println(Singleton_EagerInitialization.getInstance())).start();
    }
}
design_pattern.singleton.Singleton_EagerInitialization@70696618
design_pattern.singleton.Singleton_EagerInitialization@70696618

Process finished with exit code 0

2 Lazy initialization

public class Singleton {
    private static Singleton INSTANCE; // The instance is not created initially

    private Singleton() {} // Private constructor

    public static Singleton getInstance() { // Instance is created only when needed
        if (INSTANCE == null) { // Check if an instance has already been created
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

In this lazy initialization version, the instance is created only when getInstance() is called.

However, this implementation has thread-safety issues in a multi-threaded environment. If multiple threads call getInstance() simultaneously, they might all see INSTANCE == null, and each could create a new instance.

// This is a singleton class
// the initialization of instance is lazy
// the instance is created when the first time getInstance() is called
// the instance is shared by all threads
public class Singleton_LazyInitialization {
    private static Singleton_LazyInitialization instance;

    private Singleton_LazyInitialization() {
    }

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

    public static void main(String[] args){
        new Thread(() -> System.out.println(Singleton_LazyInitialization.getInstance())).start();
        new Thread(() -> System.out.println(Singleton_LazyInitialization.getInstance())).start();
    }
}
design_pattern.singleton.Singleton@6148e213
design_pattern.singleton.Singleton@6148e213

Process finished with exit code 0

3 DoubleCheckLazy

Thread-Safe Lazy Initialization

However, it’s important to note that since lazy initialization occurs within the getInstance() method, there can be issues in a multi-threaded environment. (It’s recommended to review the Java Util Concurrent (JUC) tutorials before implementing in concurrent settings.) Imagine a scenario where multiple threads call the getInstance() method simultaneously—what might happen in this case?

image-20230301111737649

In a multi-threaded environment, if three threads simultaneously call the getInstance() method, they will all check the INSTANCE == null condition at the same time. Since the instance hasn’t been created yet, each thread will see the condition as true, which will lead all three threads to attempt initialization. (This issue doesn’t occur with eager initialization, as the instance is created when the class loads.)

To make lazy initialization thread-safe, we can use the synchronized keyword:

public static synchronized Singleton getInstance() { // Synchronized to prevent concurrent access
    if (INSTANCE == null) {
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

Since multiple threads may call getInstance(), we can add a lock using the synchronized keyword on the method. This ensures only one thread can enter the method at a time. However, while this approach is simple and effective, it may reduce efficiency under high concurrency. To optimize, we can modify the code so that only the instantiation step is locked:

public static Singleton getInstance(){
    if(INSTANCE == null) {
        synchronized (Singleton.class) {    // Only lock the instantiation step
            INSTANCE = new Singleton();   
        }
    }
    return INSTANCE;
}

However, this isn’t perfect yet. It’s still possible for multiple threads to check INSTANCE == null and then wait for the lock. To solve this, we can add an additional inner check:

public static Singleton getInstance() {
    if (INSTANCE == null) {
        synchronized (Singleton.class) { // Lock only for initialization
            if (INSTANCE == null) {
              INSTANCE = new Singleton(); // Double-checked locking
            }
        }
    }
    return INSTANCE;
}

With this double-checked locking, only the first thread to enter will initialize the instance, ensuring that only one instance is created.

We still need to consider one more detail. In this scenario, IntelliJ IDEA likely displays a warning, suggesting that we add the volatile keyword to INSTANCE.

image-20230301111754155

The volatile keyword ensures visibility across threads, meaning that when one thread updates INSTANCE, other threads see the latest value immediately. Without volatile, there’s a chance that threads might see a partially constructed object or an outdated value, which could lead to unexpected behavior. By marking INSTANCE as volatile, we ensure that any changes to it are visible to all threads, making our Singleton implementation safer and more reliable.

So, the final version of our thread-safe Singleton with double-checked locking would look like this:

public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                  INSTANCE = new Singleton(); // Double-checked locking with volatile
                }
            }
        }
        return INSTANCE;
    }
}

This completes our Singleton with thread safety, visibility across threads, and lazy initialization.

// This is a singleton class
// the initialization of instance is lazy
// the instance is created when the first time getInstance() is called
// the instance is shared by all threads
public class Singleton_DoubleCheckLazy {
    private static Singleton_DoubleCheckLazy instance;

    private Singleton_DoubleCheckLazy() {
    }

    public static Singleton_DoubleCheckLazy getInstance(){
        if (instance == null) {// optional but recommended
            synchronized (Singleton_DoubleCheckLazy.class) {
                // synchronized 是为了保证多线程环境下只有一个线程能够进入这个代码块
                // 保证了只有一个线程能够创建实例
                // 但是这样会导致性能问题,因为每次调用getInstance()都会进入这个代码块
                // 但是只有第一次调用getInstance()的时候才需要同步
                // 所以可以使用双重检查锁定
                // Singleton_DoubleCheckLazy.class 是类的字节码对象,是类的唯一对象
                // 保证了只有一个线程能够进入这个代码块
                if (instance == null) {
                    // 如果不加这个判断,那么每次调用getInstance()都会创建一个实例
                    // 但是只有第一次调用getInstance()的时候才需要创建实例
                    instance = new Singleton_DoubleCheckLazy();
                }
            }
        }
        return instance;
    }
    // 双重检查锁定
    // 保证了只有第一次调用getInstance()的时候才需要同步
    // workflow: 首先检查instance是否为null,如果为null,才进入同步代码块
    // 进入同步代码块后,再次检查instance是否为null,如果为null,才创建实例
    // 这样保证了只有第一次调用getInstance()的时候才需要同步
    // synchronized 是为了保证多线程环境下只有一个线程能够进入这个代码块
    // 保证了只有一个线程能够创建实例

    public static void main(String[] args){
        new Thread(() -> System.out.println(Singleton_DoubleCheckLazy.getInstance())).start();
        new Thread(() -> System.out.println(Singleton_DoubleCheckLazy.getInstance())).start();
    }
}

4 Static Inner Class (Lazy Initialization)

A cleaner approach to lazy initialization without locking is to use a static inner class:

public class Singleton {
    private Singleton() {} // Private constructor prevents direct instantiation

    private static class Holder {   
        private static final Singleton INSTANCE = new Singleton(); // Singleton instance created within inner class
    }

    public static Singleton getInstance() {   
        return Holder.INSTANCE; // Returns the Singleton instance
    }
}

This approach leverages the Java class loader’s lazy loading, ensuring that the Holder class is only loaded when it’s accessed. This is a thread-safe, efficient way to implement a Singleton without locking, making it an ideal solution for lazy initialization.

Note that this method may depend on the specific language and JVM implementation, as not all languages support this pattern.