Design Pattern Course
Singleton is a Creational Design Pattern.
Sometimes it's important for a class to have exactly one instance. There are objects we only ever need one of — instantiating more than one can lead to incorrect program behavior, wasted resources, or inconsistent state (a connection pool or a configuration loader are good real-world examples).
The definition of Singleton has exactly two requirements:
- There should be only one instance allowed for the class, and
- There should be a global point of access to that single instance.
There are several ways to implement a singleton class in Java. The two generally recommended today are the static inner class (Bill Pugh) approach and the Enum approach.
1. Eager Initialization
public class EagerSingletonExample { private static final EagerSingletonExample instance = new EagerSingletonExample(); private EagerSingletonExample() { } public static EagerSingletonExample getInstance() { return instance; } public static void main(String[] args) { EagerSingletonExample ese1 = EagerSingletonExample.getInstance(); EagerSingletonExample ese2 = EagerSingletonExample.getInstance(); System.out.println(ese1 == ese2 ? "Single Object" : "Multiple Objects"); } }
Note instance is marked final — once eagerly created, the reference should never change. (Using .equals() to check identity, as the original examples did, works for a singleton but is misleading style; == is the correct and clearer way to check "is this literally the same object" — .equals() is meant for value equality, which a class might override.)
2. Lazy Initialization
public class LazySingletonExample { private static LazySingletonExample instance = null; private LazySingletonExample() { } public static LazySingletonExample getInstance() { if (instance == null) { instance = new LazySingletonExample(); } return instance; } public static void main(String[] args) { LazySingletonExample lse1 = LazySingletonExample.getInstance(); LazySingletonExample lse2 = LazySingletonExample.getInstance(); System.out.println(lse1 == lse2 ? "Single Object" : "Multiple Objects"); } }
Lazy initialization is useful when you want to defer creating an expensive object until it's actually needed, or when construction might be conditional. With eager initialization, the instance is created at class-loading time whether you ever use it or not; if construction fails, it fails at class load and there's no second chance. With lazy initialization, the instance isn't created until getInstance() is first called.
The version above is not thread-safe as written: two threads can both pass the instance == null check simultaneously and each construct their own object, silently breaking the singleton guarantee. This is a genuinely common production bug, not a theoretical concern — it's worth testing under concurrent load before trusting plain lazy initialization in a multithreaded service.
3. Double-Checked Locking
public class DoubleCheckedLockSingletonExample { private static volatile DoubleCheckedLockSingletonExample instance = null; private DoubleCheckedLockSingletonExample() { } public static DoubleCheckedLockSingletonExample getInstance() { if (instance == null) { synchronized (DoubleCheckedLockSingletonExample.class) { if (instance == null) { instance = new DoubleCheckedLockSingletonExample(); } } } return instance; } }
Simply synchronizing the entire getInstance() method works correctly, but it forces every caller to acquire a lock on every call, even after the instance already exists — unnecessary overhead for what should be a cheap getter. Double-checked locking narrows the synchronized block to only the (rare) instance-creation path: check instance == null without a lock first (cheap, the common case), and only acquire the lock — then check again — if it really does look uninitialized.
The volatile keyword here is not optional. Without it, the JVM is permitted to reorder the steps of instance = new DoubleCheckedLockSingletonExample() (allocate memory, run the constructor, assign the reference) such that another thread can observe a non-null reference to an object whose constructor hasn't finished running yet — a partially constructed object. This was a well-known bug in pre-Java-5 JVMs and is exactly why the "double-checked locking is broken in Java" advice circulated for years. Since Java 5, marking the field volatile fixes it by establishing a happens-before relationship: a write to a volatile variable is guaranteed visible to subsequent reads in other threads only after the constructor has fully completed. With volatile correctly applied, double-checked locking on the JVM has been safe since Java 5 (2004) — but it's easy to get wrong, which is exactly why approaches 4 and 5 below are generally preferred over hand-rolling this pattern.
4. Initialization-on-Demand Holder (Static Inner Class)
Bill Pugh's approach uses a static inner class to get lazy initialization without any explicit synchronization:
public class InnerClassSingletonExample { private InnerClassSingletonExample() { } private static class SingletonHolder { private static final InnerClassSingletonExample INSTANCE = new InnerClassSingletonExample(); } public static InnerClassSingletonExample getInstance() { return SingletonHolder.INSTANCE; } }
This works because the JVM doesn't load and initialize the nested SingletonHolder class until getInstance() is called for the first time, and the JVM's class-loading mechanism is already thread-safe by specification — so you get lazy initialization, full thread safety, and no manual synchronization or volatile, with no performance penalty after the first call. This is generally the best plain-Java approach if you need lazy initialization and aren't using Enum.
5. Using Enum
Joshua Bloch (in Effective Java) recommends using an enum to implement Singleton, since the JVM guarantees an enum constant is instantiated exactly once, and that guarantee holds even against reflection-based attacks and serialization — both of which can break the other four approaches above unless you add extra defensive code (e.g., overriding readResolve(), or having the constructor throw on a second invocation). The trade-off is that an enum can't extend another class (though it can implement interfaces), and it doesn't support lazy initialization in the same fine-grained way.
public enum EnumSingletonExample { INSTANCE; public static void main(String[] args) { EnumSingletonExample ese1 = EnumSingletonExample.INSTANCE; EnumSingletonExample ese2 = EnumSingletonExample.INSTANCE; System.out.println(ese1 == ese2 ? "Single Object" : "Multiple Objects"); } }
Benefits
- Guarantees at most one instance of the class exists.
- Provides a single, well-known global access point.
- Useful for coordinating access to a shared, expensive-to-create resource (a thread pool, a connection pool, a cache).
Limitations
- It's a form of global mutable state, which makes unit testing harder — singletons are difficult to mock or substitute with a test double unless the class is designed for it, and tests can accidentally leak state into one another through a shared singleton instance.
- It can hide dependencies: a method that quietly calls
Singleton.getInstance()internally has a hidden dependency that isn't visible in its constructor or method signature, which makes the codebase harder to reason about. - It introduces tight coupling to a single concrete implementation, which works against the Dependency Inversion Principle.
- The naive lazy-initialization version is not thread-safe by default (see above); making it correctly thread-safe takes care (or one of the two recommended approaches above).
- If the singleton is mutable, it becomes implicit shared state across the whole application, including across unrelated requests in a multi-threaded server — a frequent source of subtle bugs.
In modern enterprise Java, hand-rolling the Singleton pattern is increasingly rare. Frameworks like Spring manage object lifecycle for you: a @Component, @Service, or @Bean is singleton-scoped within the Spring container by default, giving you the "one instance, one access point" behavior without any of the boilerplate above — and, critically, the container injects it as a dependency rather than having your code reach out and grab it via a static getInstance() call, which keeps the class testable and the dependency explicit. For that reason, many architects consider hand-written Singletons (as opposed to container-managed singleton-scoped beans) something to reach for sparingly — genuinely useful for things like loggers, JDBC drivers, and certain low-level caches, but easy to overuse as a substitute for proper dependency injection.
Singleton is commonly used for logging frameworks, database driver/connection-pool objects, caches, and thread pools.
Singleton Examples in the JDK
java.lang.Runtime.getRuntime()— returns the singleRuntimeinstance associated with the current JVM.java.awt.Desktop.getDesktop()— returns the singletonDesktopinstance for the current platform.java.lang.System.getSecurityManager()— this one is no longer a meaningful Singleton example: the Security Manager and related APIs were deprecated for removal in Java 17 (JEP 411) and removed entirely in Java 24, so this is now outdated as a "current" JDK example, though it's still worth knowing historically.