DEV Community

Cover image for Understanding the Singleton Pattern in Java
sachinkg12
sachinkg12

Posted on

1

Understanding the Singleton Pattern in Java

The Singleton Pattern is one of the most commonly used design patterns in Java. It ensures that a class has only one instance and provides a global point of access to that instance.

Think of it like a manager in a team, there is only one manager for a specific team, and everyone in the other department communicates through them.

Let’s break it down in simple terms and understand how you can implement this in Java.


Why Use the Singleton Pattern?

  • Single Instance: To ensure that there is only one instance of a class in your application.

    • For example, think of a database connection pool, having a single instance ensures efficient reuse of connections without repeatedly creating and destroying them.
    • Another example is a printer spooler that manages print jobs across the entire system, ensuring there’s no conflict between users.
  • Global Access Point: To provide a single, shared point of access to the instance.

  • Resource Management: To manage shared resources like configurations, logging, or thread pools efficiently.


How to Implement Singleton Pattern in Java

1. Lazy Initialization Singleton

This approach initializes the instance only when it’s needed.

import java.io.Serializable;
public class LazySingleton implements Serializable {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Visualization:

Imagine a library where a librarian only sets up a table when a visitor arrives:

  • The if (instance == null) check is like the librarian checking if there’s already a table set up.
  • If no table is set, they quickly arrange one (create the instance).
  • The next visitors reuse the same table without additional setup.

Problems and Fixes:

  • Serialization Issue:

    • Breaking Example: Without readResolve, deserialization creates a new instance.
    import java.io.*;
    
    public class TestSerialization {
        public static void main(String[] args) throws Exception {
            LazySingleton instance1 = LazySingleton.getInstance();
    
            // Serialize the instance
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
            oos.writeObject(instance1);
            oos.close();
    
            // Deserialize the instance
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
            LazySingleton instance2 = (LazySingleton) ois.readObject();
            ois.close();
    
            System.out.println(instance1 == instance2); // false
        }
    }
    
    • Fix: Add readResolve to ensure the same instance is returned.
    private Object readResolve() {
        return getInstance();
    }
    
  • Reflection Issue:

    • Breaking Example: Reflection can call the private constructor.
    import java.lang.reflect.Constructor;
    
    public class TestReflection {
        public static void main(String[] args) throws Exception {
            LazySingleton instance1 = LazySingleton.getInstance();
    
            Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            LazySingleton instance2 = constructor.newInstance();
    
            System.out.println(instance1 == instance2); // false
        }
    }
    
    • Fix: Add a guard in the constructor to throw an exception if an instance already exists.
    private LazySingleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already created");
        }
    }
    

2. Thread-Safe Singleton

To make the Singleton thread-safe, you can use the synchronized keyword.

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Visualization:

Imagine a bakery where only one oven can bake bread at a time:

  • The synchronized keyword acts as a lock on the oven, ensuring only one baker can use it to bake bread.
  • When the first loaf (instance) is ready, all other bakers can simply reuse it.

Problems and Fixes:

  • Serialization Issue:

    • Breaking Example: Without readResolve, deserialization creates a new instance.
    public class TestPerformance {
    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                ThreadSafeSingleton.getInstance();
            }
        };
    
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
    
        long start = System.nanoTime();
        t1.start();
        t2.start();
    
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        long end = System.nanoTime();
        System.out.println("Execution Time: " + (end - start) + " ns");
    }
    }
    
    • Fix: Use double-checked locking to reduce synchronization overhead:
    public static ThreadSafeSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
    
  • Reflection Issue:

    • Fix: Add a guard in the constructor:
    private ThreadSafeSingleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already created");
        }
    }
    

3. Double-Checked Locking Singleton

This approach minimizes the performance cost of synchronization.

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {}

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Visualization:

Imagine a bank vault:

  • The first if condition is like a security camera checking if the vault is already open.
  • The synchronized block is like a guard only letting one person unlock the vault at a time.
  • The second if ensures the vault isn’t opened twice by mistake.

Problems and Fixes:

  • Serialization Issue:

    • Same fix as Lazy Singleton.
  • Reflection Issue:

    • Same fix as Lazy Singleton.

4. Bill Pugh Singleton (Recommended)

public class BillPughSingleton {
    private BillPughSingleton() {}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
Enter fullscreen mode Exit fullscreen mode

Visualization:

Think of a vending machine with a coin slot that automatically creates change when needed:

  • The SingletonHelper class is like a hidden compartment in the vending machine, pre-loaded with the mechanism to give change.
  • Only when someone inserts a coin (calls getInstance) does the mechanism activate, dispensing the Singleton instance.

Problems and Fixes:

  • Serialization Issue:

    • Breaking Example: Without readResolve, deserialization creates a new instance.
    import java.io.*;
    
    public class TestSerialization {
        public static void main(String[] args) throws Exception {
            BillPughSingleton instance1 = BillPughSingleton.getInstance();
    
            // Serialize the instance
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
            oos.writeObject(instance1);
            oos.close();
    
            // Deserialize the instance
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
            BillPughSingleton instance2 = (BillPughSingleton) ois.readObject();
            ois.close();
    
            System.out.println(instance1 == instance2); // false
        }
    }
    
    • Fix: Add readResolve to ensure the same instance is returned.
    private Object readResolve() {
        return getInstance();
    }
    
  • Reflection Issue:

    • Fix: Add a guard in the constructor:
    private BillPughSingleton() {
        if (SingletonHelper.INSTANCE != null) {
            throw new IllegalStateException("Instance already created");
        }
    }
    

5. Singleton Using Enum (Modern Approach)

With newer Java versions (Java 8 and beyond), you can use Enums to implement Singleton. Enums are inherently thread-safe and handle serialization by default.

public enum EnumSingleton {
    INSTANCE;

    public void showMessage() {
        System.out.println("Hello from Singleton!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Visualization:

Picture a monarch (king or queen) who is the sole ruler of their kingdom:

  • The INSTANCE is like the monarch, inherently unique and impossible to duplicate.
  • The JVM ensures that no matter how many times you call the monarch, you always get the same ruler.

Final Versions of Singleton Patterns with fixes


1. Lazy Initialization Singleton

import java.io.*;

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

    private LazySingleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already created");
        }
    }

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

    private Object readResolve() {
        return getInstance();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Thread-Safe Singleton

import java.io.*;

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

    private ThreadSafeSingleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already created");
        }
    }

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

    private Object readResolve() {
        return getInstance();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Double-Checked Locking Singleton

import java.io.*;

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

    private DoubleCheckedLockingSingleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already created");
        }
    }

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

    private Object readResolve() {
        return getInstance();
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Bill Pugh Singleton (Recommended)

import java.io.*;

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

    private BillPughSingleton() {
        if (SingletonHelper.INSTANCE != null) {
            throw new IllegalStateException("Instance already created");
        }
    }

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

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

    private Object readResolve() {
        return getInstance();
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Singleton Using Enum (Modern Approach)

public enum EnumSingleton {
    INSTANCE;

    public void showMessage() {
        System.out.println("Hello from Enum Singleton!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • Lazy Initialization: Simple, but not thread-safe without additional fixes.
  • Thread-Safe Singleton: Ensures thread safety but may incur performance overhead.
  • Double-Checked Locking: Reduces synchronization overhead while remaining thread-safe.
  • Bill Pugh Singleton: Recommended for its simplicity, efficiency, and elegance.
  • Enum Singleton: Modern and robust, suitable for most use cases.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay