Singleton Design Pattern in Java: 6 Implementations with Code Examples

Image from https://unsplash.com/.

Overview

1. Introduction

Singleton is a Creational Design Pattern defined in the Gang-of-Four Book to solve known problems of objects' creation.

In this tutorial, we'll see 6 different implementations of a Singleton class in Java and check the pros and cons of each one.

Find the entire source code for this tutorial over on GitHub.

2. What is the Singleton Pattern

Singleton means a "single thing" in English.

In Java and other object-oriented languages, that "single thing" is the instance of a class.

In short, Singletons are classes that are only allowed to have one instance (or object) during the program execution. Let's use a diagram to illustrate a Singleton class:

A UML diagram for the Singleton Design Pattern

Note: Use that simple UML Guide to get used to the notation used in the diagram.

Let's summarize what's going on in the diagram above:

  • SingletonClass creates an object of itself once and stores it in the instance field.
  • getInstance() always returns the same object stored in instance.
  • The constructor and instance are declared as private to forbid any access outside SingletonClass.
  • instance and getInstance() should be declared static to enable access to them directly from the class name.

2.1. Where is Singleton Pattern Used in the JDK

One of the best examples of Singleton is the Java Logger API. Log structures don't change during the application execution, which makes it possible to share the same logger object with the entire application.

Another example is the Runtime class that uses a Singleton to manage the relationship between the application and the environment in which the application is running.

Singletons create a unique entry point to a shared resource used across the entire application.

3. How to Create a Singleton Pattern in Java

Singleton is easy-to-implement because it uses a single class with one method. However, nuances like thread safety and field initialization need different implementations.

In this section, we'll review 6 implementations of the Singleton pattern in Java and compare each.

3.1. Naive Lazy Initialization

That implementation creates the object only when the getInstance() method is called by another class:

 1public class LazyInitSingleton {
 2
 3    private static LazyInitSingleton instance;
 4
 5    private LazyInitSingleton () {
 6    }
 7
 8    public static LazyInitSingleton getInstance() {
 9        if (instance == null) {
10            instance = new NaiveLazyInitSingleton ();
11        }
12        return instance;
13    }
14}

The JVM loads the LazyInitSingleton class with the instance field equal to null. When we call getInstance(), the JVM creates the object using our private constructor only if the reference to instance is null.

That approach works well in single-thread environments. However, if multiple threads access the if conditional simultaneously, it is possible that more than one instance of LazyInitSingleton is created.

We'll address the race condition problem using locks in the following sections.

3.2. Synchronized Lazy Initialization

To handle race conditions, we must lock the access to the if statement at LazyInitSingleton to avoid two threads accessing that code simultaneously. Let's use the synchronized keyword at the getInstance() method level to create that lock:

 1public class SynchronizedLazyInitSingleton {
 2
 3    private static SynchronizedLazyInitSingleton instance;
 4
 5    private SynchronizedLazyInitSingleton() {
 6    }
 7
 8    public static synchronized SynchronizedLazyInitSingleton getInstance() {
 9        if (instance == null) {
10            instance = new SynchronizedLazyInitSingleton();
11        }
12        return instance;
13    }
14}

The only difference between the code above and the naive approach is the synchronized keyword. That keyword ensures that only one thread simultaneously accesses the getInstance() method. Thus, making it impossible to create more than one singleton object.

3.3. Double-Checked Locking

Even though the prior approach solves the race condition, there's one performance pitfall: each time a thread tries to create a Singleton, that thread needs to evaluate a potentially unnecessary lock, which creates an overhead.

We can implement a double-checked locking to cease those overheads. The double-checked approach first checks if the object needs to be created before trying to acquire a lock. Let's see how that works in practice:

 1public class DoubleCheckedLockingSingleton {
 2
 3    private static volatile DoubleCheckedLockingSingleton instance;
 4
 5    private DoubleCheckedLockingSingleton() {
 6    }
 7
 8    public static DoubleCheckedLockingSingleton getInstance() {
 9        if (instance == null) {
10            synchronized (DoubleCheckedLockingSingleton.class) {
11                if (instance == null) {
12                    instance = new DoubleCheckedLockingSingleton();
13                }
14            }
15        }
16
17        return instance;
18    }
19}

That is the most verbose and complicated solution we'll see in this tutorial, so let's break it into parts:

  • The first if statement checks if the variable was initialized to avoid entering the synchronized block.
  • The thread tries to obtain the lock using the synchronized keyword.
  • The second conditional is a double check to determine if the variable was already initialized.
  • We use the volatile keyword to avoid getting cached results by the JVM and guarantee the object's consistency.

That approach seems uncommon, but most of the code is to implement some guarantees in multi-threaded environments, particularly synchronization and atomicity.

3.4. Bill Pugh's or Holder Class Singleton

That approach does the same as the double-checked locking solution in a more elegant way. It's called Bill Pugh's Singleton as a tribute to its creator.

In short, that approach uses a static inner class to hold and initialize the instance variable. When we first reference the inner class, the class loader automatically creates the object.

Let's take a look at Bill Pugh's Singleton implementation in Java using a static inner class:

 1public class BillPughSingleton {
 2    private BillPughSingleton() {
 3    }
 4
 5    private static final class InstanceHolder {
 6        private static final BillPughSingleton instance = new BillPughSingleton();
 7    }
 8
 9    public static BillPughSingleton getInstance() {
10        return InstanceHolder.instance;
11    }
12}

The InstanceHolder inner class contains the initializer of the BillPughSingleton class. When we access the getInstance(), the compiler loads the holder class and creates the object.

Using Bill Pugh's lazy approach, we guarantee that only one singleton object exists throughout the program in a thread-safe manner.

3.5. Eager Initialization

That approach uses the JVM class loader to create the object instance and assign it to a static final variable. Let's check that out:

 1public class EagerInitSingleton {
 2
 3    private static final EagerInitSingleton instance = new EagerInitSingleton();
 4
 5    private EagerInitSingleton() {
 6    }
 7
 8    public static EagerInitSingleton getInstance() {
 9        return instance;
10    }
11}

When the JVM loads the EagerInitSingleton class, it also calls the constructor since the variable is static. So, we guarantee that the variable is assigned only once during the program and that it can't be modified because of the final modifier.

3.6. Enum Singleton

We can use an Enum to replicate the same eager initialization mentioned previously but without making it explicit in the code. The Enum type does all the job for us:

1public enum EnumSingleton {
2
3    INSTANCE;
4    
5    //do other stuff
6}

The JVM eagerly creates the instance of Enum at the class loading time with an auto-generated private constructor. In previous approaches, the instance field is equivalent to Enum's value INSTANCE.

4. Comparison of Different Implementations

This section compares the approaches mentioned so far, considering three aspects: lazy vs. eager initialization, thread safety, and simplicity.

4.1. Pros and Cons of Eager vs. Lazy Initialization

Choosing between creating the object eagerly or lazily depends on the application's requirements and the purpose of the singleton class.

Pros of Eager Initizalition:

  • The object is available right after the application starts.
  • It doesn't require any synchronization code to make it thread-safe.

Cons of Eager Initialization:

  • We're potentially creating an unnecessary object.
  • We might create some slowness in the application start for massive objects.

Pros of Lazy Initialization:

  • The object is available only when we instantiate it in runtime.

Cons of Lazy Initialization:

  • It needs explicit synchronization code.
  • There's a performance overhead due to additional if statements and/or synchronized block.

The real question to answer to choose between eager or lazy initialization is: do we need the object immediately after the application starts, or can we create its instance during the application execution?

4.2. Thread-Safety

All eager approaches are naturally thread-safe, so we don't need any synchronization block to work with them effectively in multi-threaded environments. That's the case for both EagerInitSingleton and EnumSingleton classes.

On the other hand, lazy approaches require synchronization code to work correctly in multi-threaded environments. Summarizing lazy approaches:

  • The naive LazyInitSingleton works poorly in multi-threaded environments because we might create more than one singleton object.

  • The SynchronizedLazyInitSingleton is thread-safe, but it creates a performance overhead due to the potentially unnecessary call to the synchronized block.

  • The DoubleCheckedLockingSingleton and BillPughSingleton achieve the same in thread safety without diminishing perfomance.

4.3. Simplicity

Simplicity is also a good point for choosing between different implementations of Singleton.

We'll analyze only the thread-safe optimal approaches to compare simplicity. Thus, we have left eager approaches, Bill Pugh's solution, and the double-checked strategy.

For eager approaches, it looks like the enum approach wins over the traditional eager one because we need much less code. Some may argue that Enum hides most of the source code, making it harder to read. In the end, it's a matter of preference.

For lazy approaches, Bill Pugh's solution is the way to go if static inner class is a well-understood concept to anyone who maintains the code. However, synchronized code is a widely misused concept in Java applications, making the double-checked locking solution a bit challenging to understand. In terms of code length, Bill Pugh's wins as it is much lesser verbose.

5. Conclusion

In this tutorial, we've first defined the Singleton Pattern and its purpose in software design.

We've also seen many implementations for the Singleton Design Pattern in Java with code examples.

The idea we need to keep in mind is always to evaluate the problem we want to solve. If the Singleton pattern is a good match for that problem, now we must choose an implementation that best fits the context of our application.

Find the complete source code for this tutorial at GitHub.

Posts in this Series