The Singleton is a creational Design Pattern whose purpose is to guarantee only one instance of the class gets created and, in order to achieve it, there are a couple of directives that one must follow. In the following sections, we will see all the options that you have when it comes to implementing the Singleton Design Pattern in Java programming language.
1. Overview
There are some common features that every Singleton shares regardless of whichever implementation you may use in your code.
- They all must have a private or protected constructor so as to prevent instantiation and prevent subclassing.
- A static
getInstance()
method is essential and here is where logic goes. - There must be only one Singleton instance for the entire application.
- They all have a static variable of the Singleton’s type that points to the instance.
- Preferably make the class final to prevent subclassing and improve performance.
2. Singleton Design Pattern in Java with Lazy Initialization
Probably the most common Singleton implementation out there and it relies on postponing initialization until its first access, then it creates an instance of itself which stays in memory for the duration of the application’s lifetime.
Advantages
- Laziness: creates the
instance
only when actually needed. - Simplicity: very easy and simple to implement.
- Compatibility: compatible with all versions of Java.
- Performance: does not require synchronization, double-checks or inner classes.
Disadvantages
- Not Thread-safe: the main disadvantage is that it is not thread-safe. Not a problem in single-thread applications.
- Reflection: susceptible to reflection attacks.
- Limitation: limited to single-threaded applications only.
package com.youlearncode; public final class LazyInitialization { private static LazyInitialization instance; private LazyInitialization() { } public static LazyInitialization getInstance() { if (instance == null) instance = new LazyInitialization(); return instance; } }
This implementation works well for single-threaded applications, and should never be used in a multithreading environment because it could cause serious issues.
Let’s simulate a multi-threading application and what could happen if one were to use this implementation in a concurrent environment.
private static void lazyInitialization() { Runnable runnable = () -> { LazyInitialization lazySingleton = LazyInitialization.getInstance(); System.out.println(lazySingleton); }; for (int i = 0; i < 5; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.LazyInitialization@7d4443b1 com.youlearncode.LazyInitialization@40082b1e com.youlearncode.LazyInitialization@40082b1e com.youlearncode.LazyInitialization@40082b1e com.youlearncode.LazyInitialization@40082b1e
As you can see, our Singleton failed and two instances were created, which becomes evident by the code after the class name: @7d4443b1 for the first and @40082b1e for the other four.
3. Lazy Initialization (Synchronized)
Here the only change is the synchronized
keyword added to the getInstance()
method.
Advantages
- Laziness: creates the
instance
only when actually needed. - Thread-safety: inner class solves concurrency issues without the need for synchronization.
- Simplicity: very easy and simple to implement.
- Compatibility: compatible with all versions of Java.
Disadvantages
- Reflection: susceptible to reflection attacks.
- Inefficiency: it creates a synchronization lock for every call to
getInstance()
. - Overhead: synchronized methods may cause the application to slow down, especially in high-concurrent environments.
package com.youlearncode; public final class SynchronizedLazyInitialization { private static SynchronizedLazyInitialization instance; private SynchronizedLazyInitialization() { } public static synchronized SynchronizedLazyInitialization getInstance() { if (instance == null) instance = new SynchronizedLazyInitialization(); return instance; } }
Again, let’s simulate a multithreading environment and check the output, but this time, we will iterate 10 times.
private static void synchronizedLazyInitialization() { Runnable runnable = () -> { SynchronizedLazyInitialization synchronizedLazyInitialization = SynchronizedLazyInitialization.getInstance(); System.out.println(synchronizedLazyInitialization); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2 com.youlearncode.SynchronizedLazyInitialization@1b623ba2
This time our code succeeded in guaranteeing that just one instance is available for the whole application.
4. Lazy Initialization with Double Check Lock
In the previous implementation, the whole method is synchronized which may lead to performance issues. Now, if optimization is a concern, there is an improved version where a synchronized
block is used only when first accessed. Do note the volatile
keyword.
Advantages
- Laziness: creates the
instance
only when actually needed. - Thread-safety: inner class solves concurrency issues without the need for synchronization.
- Performance: it is considered to be more performant than previous implementation.
Disadvantages
- Reflection: susceptible to reflection attacks.
- Compatibility: JDK 5 brought a new memory model and
Thread
specification that fixed the broken double-check problem with thevolatile
keyword. - Synchronization: requires
synchronized
andvolatile
keywords to work.
package com.youlearncode; public final class LazyInitializationWithDoubleCheckLock { // The volatile keyword ensures that multiple threads handle // the instance variable correctly when it is being initialized. private static volatile LazyInitializationWithDoubleCheckLock instance; private LazyInitializationWithDoubleCheckLock() { } public static LazyInitializationWithDoubleCheckLock getInstance() { if (instance == null) synchronized (LazyInitializationWithDoubleCheckLock.class) { if (instance == null) // Double-checking for null instance = new LazyInitializationWithDoubleCheckLock(); } return instance; } }
For testing purposes let’s add some threads and run.
private static void lazyInitializationWithDoubleCheckLock() { Runnable runnable = () -> { LazyInitializationWithDoubleCheckLock lazyInitializationWithDoubleCheckLock = LazyInitializationWithDoubleCheckLock.getInstance(); System.out.println(lazyInitializationWithDoubleCheckLock); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db com.youlearncode.LazyInitializationWithDoubleCheckLock@3bc0a9db
5. Lazy Initialization with Inner Class (Bill Pugh)
This is an improved version of the previous implementation created by Bill Pugh where you get the benefit of the lazy initialization without the need for synchronization. Instead, it uses an inner static class (The Holder) to lazily load the Singleton.
It also goes by other names such as: Initialization-On-Demand Holder idiom or simply IODH and Lazy initialization Holder Class idiom.
Advantages
- Laziness: creates the
instance
only when actually needed. - Thread-safety: inner class solves concurrency issues without the need for synchronization.
- Compatibility: compatible with all versions of Java.
- Performance: more performant than implementations than uses synchronization.
Disadvantages
- Overhead: inner class can result in additional memory use and cause performance issues.
- Reflection: susceptible to reflection attacks.
- Complexity: it is a bit more complex than other implementations and requires deeper knowledge of the language.
- Serialization: whenever we deserialize it, it creates a new
instance
of the Singleton.
package com.youlearncode; public final class LazyInitializationWithInnerClass implements Singleton { private LazyInitializationWithInnerClass() { } private static final class InstanceHolder { private static final LazyInitializationWithInnerClass instance = new LazyInitializationWithInnerClass(); } public static LazyInitializationWithInnerClass getInstance() { return InstanceHolder.instance; } }
This implementation is widely used and it is the recommended one to implement Singleton in Java.
private static void lazyInitializationWithInnerClass() { Runnable runnable = () -> { LazyInitializationWithInnerClass lazyInitializationWithInnerClass = LazyInitializationWithInnerClass.getInstance(); System.out.println(lazyInitializationWithInnerClass); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750 com.youlearncode.LazyInitializationWithInnerClass@4124d750
6. Singleton Design Pattern in Java Eager Initialization
Simplicity is where this implementation shines given the instance is created at class loading time and there is no need for synchronization. On the other hand, its major drawback is that it creates the instance
whether one accesses it or not, which shouldn’t be a problem if one is not dealing with a complex object and it does not hold many or any system resources. Otherwise, it may lead to performance issues.
Advantages
- Simplicity: code is simple and easy to read and understand and does not require any additional resources.
- Thread-safe: inherently thread-safe. The instance gets created before any thread may have a chance to access it.
- Availability: the
instance
is available at all times for the application’s lifetime.
Disadvantages
- Resource: as soon as the class is loaded into memory the
instance
becomes available and stays in memory for the application’s lifetime resulting in unnecessary memory usage. The larger the more expensive it will be. - Time: initialization time may be compromised depending on the complexity and size of the
instance
. - Inflexibility: there is no way to create a more flexible Singleton since it creates the
instance
(usuallyfinal
) at loading time which remains for as long as the application is running. - Reflection: susceptible to reflection attacks.
package com.youlearncode; public final class EagerInitialization implements Singleton { private static final EagerInitialization instance = new EagerInitialization(); private EagerInitialization() { } public static EagerInitialization getInstance() { return instance; } }
As you can see, simplicity is key here and there won’t be concurrency issues.
private static void eagerInitialization() { Runnable runnable = () -> { EagerInitialization eagerSingleton = EagerInitialization.getInstance(); System.out.println(eagerSingleton); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692 com.youlearncode.EagerInitialization@55731692
7. Eager Initialization with Static Block
This works because the JVM loads and executes static blocks first (during class loading).
Advantages
- Simplicity: code is simple and easy to read and understand. The
instance
is available at all times. - Thread-safe: JVM guarantees that a static block only runs once at class loading time.
- Error-handling: static blocks allow you to handle and log errors.
- Availability: the
instance
is available at all times for the application’s lifetime. - Customizable: it is more flexible than the previous implementation. Your
instance
may be different based on error handling or configuration.
Disadvantages
- Resource: as soon as the class is loaded, the static block creates an instance which stays in memory for the application’s lifetime resulting in unnecessary memory usage. The larger the more expensive it will be.
- Time: initialization time may be compromised depending on the complexity and size of the
instance
. - Testing: unit-testing can be challenging.
- Reflection: susceptible to reflection attacks.
package com.youlearncode; import com.youlearncode.exceptions.SingletonException; public final class EagerInitializationWithStaticBlock { private static final EagerInitializationWithStaticBlock instance; static { try { instance = new EagerInitializationWithStaticBlock(); } catch (Exception e) { // log error throw new SingletonException(e.getMessage()); } } private EagerInitializationWithStaticBlock() { } public static EagerInitializationWithStaticBlock getInstance() { return instance; } }
As you can see, it is still possible to have a final
instance using a static block.
private static void eagerInitializationWithStaticBlock() { Runnable runnable = () -> { EagerInitializationWithStaticBlock eagerInitializationWithStaticBlock = EagerInitializationWithStaticBlock.getInstance(); System.out.println(eagerInitializationWithStaticBlock); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430 com.youlearncode.EagerInitializationWithStaticBlock@4de4d430
8. Enum Singleton (The Natural Singleton)
Enums are natural Singletons which makes them one of the best options for eagerly initialized Singletons.
Advantages
- Thread-safety: enums are inherently thread-safe and do not require synchronization.
- Serialization: built-in support for serialization and deserialization.
- Anti-reflection: protection against reflection attacks.
- Straightforwardness: code is simple and concise.
- Performance: better performance compared to other implementations Eager Initialization and Static Block.
Disadvantages
- Extensibility: enums are not extensible and therefore you cannot subclass them.
- Configurability: there isn’t much room for configuration if you need dynamism.
- Compatibility: not compatible with JDK versions below 5.
package com.youlearncode; public enum EnumSingleton { INSTANCE("Singleton"); private final String singleton; EnumSingleton(String singleton) { this.singleton = singleton; } public String getSingleton() { return singleton; } }
And just to keep it concise, we will add the toString()
method to return the class name plus the hash code.
@Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
Here our singleton takes a String, but it could take as many parameters as you need it to.
private static void enumSingleton() { Runnable runnable = () -> { EnumSingleton enumSingleton = EnumSingleton.INSTANCE; System.out.println(enumSingleton); }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } }
Output:
com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f com.youlearncode.EnumSingleton@108b360f
9. Conclusion
By now you should be familiar with all implementations of the Singleton Design Pattern in Java, their advantages and disadvantages, and which ones are widely used in the community. Check out our GitHub Page for the source code.
10. Sources
[1]: Effective Ways to Implement and Use the Singleton Design Pattern.