Hi! Today we'll dive into the details of various design patterns, starting with the Java Singleton pattern. Let's review: what do we know about design patterns in general? Design patterns are best practices that we can apply to solve a number of known problems. Design patterns are generally not tied to any programming language. Think of them as a set of recommendations to help you avoid mistakes and avoid reinventing the wheel.

What is a singleton in Java?

Singleton is one of the simplest class-level design patterns. Sometimes people say "this class is singleton”, which means that the class implements the singleton design pattern. Sometimes it is necessary to write a class where we restrict instantiation to a single object. For example, a class responsible for logging or connecting to a database. The singleton design pattern describes how we can achieve this. A singleton is a design pattern that does two things:
  1. It guarantees that there will only ever be one instance of the class.

  2. It provides a single point of global access to that instance.

Hence, there are two features that are characteristic of nearly every implementation of the singleton pattern:
  1. A private constructor. This limits the ability to create objects of the class outside of the class itself.

  2. A public static method that returns the instance of the class. This method is called getInstance. This is the point of global access to the class instance.

Design patterns: Singleton - 1

Implementation options

The singleton design pattern is applied in various ways. Each option is good and bad in its own way. As always, there is no perfect option here, but we should strive for one. First of all, let's decide what constitutes good and bad, and what metrics affect how we assess the various implementations of the design pattern. Let's start with the good. Here are factors that make an implementation more juicy and appealing:
  • Lazy initialization: the instance is not created until it is needed.

  • Simple and transparent code: this metric, of course, is subjective, but it is important.

  • Thread safety: correct operation in a multi-threaded environment.

  • High performance in a multi-threaded environment: little or no thread blocking when sharing a resource.

Now the cons. We'll list factors that put an implementation in a bad light:
  • No lazy initialization: when the class is loaded when the application starts, regardless of whether or not it is needed (paradoxically, in the IT world it is better to be lazy)

  • Complex and difficult-to-read code. This metric is also subjective. If your eyes start bleeding, we'll assume the implementation isn't the best.

  • Lack of thread safety. In other words, "thread danger". Incorrect operation in a multi-threaded environment.

  • Poor performance in a multi-threaded environment: threads block each other all the time or often when sharing a resource.

Code

Now we're ready to consider various implementation options and indicate the pros and cons:

Simple


public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
The simplest implementation. Pros:
  • Simple and transparent code

  • Thread safety

  • High performance in a multi-threaded environment

Cons:
  • No lazy initialization.
In an attempt to fix the previous shortcoming, we get implementation number two:

Lazy initialization


public class Singleton {
  private static final Singleton INSTANCE;

  private Singleton() {}

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Pros:
  • Lazy initialization.

Cons:
  • Not thread safe

This implementation is interesting. We can initialize lazily, but we've lost thread safety. No worries — we synchronize everything in implementation number three.

Synchronized access


public class Singleton {
  private static final Singleton INSTANCE;

  private Singleton() {
  }

  public static synchronized Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Pros:
  • Lazy initialization.

  • Thread safety

Cons:
  • Poor multithreaded performance

Excellent! In implementation number three, we restore thread safety! Of course, it's slow... Now the getInstance method is synchronized, so it can be executed by only one thread at a time. Rather than synchronize the entire method, we actually only need to synchronize the part of it that initializes the new instance. But we can't simply use a synchronized block to wrap the part responsible for creating the new instance. Doing that would not ensure thread safety. It's all a bit more complicated. Proper synchronization can be seen below:

Double-checked locking


public class Singleton {
    private static final Singleton INSTANCE;

  private Singleton() {
  }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
Pros:
  • Lazy initialization.

  • Thread safety

  • High performance in a multi-threaded environment

Cons:
  • Not supported in earlier versions of Java below 1.5 (the usage of volatile keyword is fixed since the 1.5 version)

Note that in order for this implementation option to work correctly, one of two conditions must be satisfied. The INSTANCE variable must be either final or volatile. The last implementation that we'll discuss today is the class holder singleton.

Class holder


public class Singleton {

   private Singleton() {
   }

   private static class SingletonHolder {
       public static final Singleton HOLDER_INSTANCE = new Singleton();
   }

   public static Singleton getInstance() {
       return SingletonHolder.HOLDER_INSTANCE;
   }
}
Pros:
  • Lazy initialization.

  • Thread safety.

  • High performance in a multi-threaded environment.

Cons:
  • Correct operation requires a guarantee that the singleton object is initialized without errors. Otherwise, the first call to the getInstance method will result in an ExceptionInInitializerError, and all subsequent calls will produce a NoClassDefFoundError.

This implementation is almost perfect. It is lazy, and thread safe, and fast. But it has a nuance, as explained in the list of cons. Comparison of various implementations of the singleton pattern:
Implementation Lazy initialization Thread safety Multithreaded performance When to use?
Simple - + Fast Never. Or possibly when lazy initialization is not important. But never would be better.
Lazy initialization + - Not applicable Always when multithreading is not needed
Synchronized access + + Slow Never. Or possibly when multithreaded performance does not matter. But never would be better.
Double-checked locking + + Fast In rare cases when you need to handle exceptions when creating the singleton (when the class holder singleton is not applicable)
Class holder + + Fast Whenever multithreading is needed and there is a guarantee that the singleton object will be created without problems.

Eager Initialization

In Eager Initialization, the Singleton instance is created as soon as the class is loaded. This means the instance is ready to use even before it's needed. It’s like making a cup of coffee in the morning just in case you might want it later. No waiting—just grab and go!

Syntax of Eager Initialization

Let’s look at how this works in Java:

public class EagerSingleton {

    // Instance is created when the class is loaded
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // Private constructor prevents instantiation from other classes
    private EagerSingleton() {
        System.out.println("EagerSingleton instance created!");
    }

    // Public method to provide access to the instance
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

Usage Example:

public class Main {
    public static void main(String[] args) {
        // Accessing the Singleton instance
        EagerSingleton instance1 = EagerSingleton.getInstance();
        EagerSingleton instance2 = EagerSingleton.getInstance();

        System.out.println(instance1 == instance2);  // Output: true
    }
}

Output:

EagerSingleton instance created!
true

Notice that the message "EagerSingleton instance created!" is printed only once. Both instance1 and instance2 point to the same object. That’s the Singleton magic!

Pros and Cons of Eager Initialization

Now that you’ve seen how easy it is to implement, let’s weigh the pros and cons of eager initialization.

Advantages:

  • Simplicity: It’s super easy to implement. No complex synchronization or multi-threading concerns.
  • Thread Safety: Since the instance is created during class loading, it’s inherently thread-safe without needing extra code.
  • Reliable Access: The instance is always available when you need it. No null checks required!

Disadvantages:

  • Resource Waste: If the instance is never used, the memory and resources allocated for it are wasted.
  • Lack of Flexibility: You can’t delay the creation of the instance, even if it’s expensive to create.

So, when should you use eager initialization? If the Singleton object is lightweight and will definitely be used, this method is perfect. Otherwise, consider lazy initialization for better resource management.

Eager vs. Lazy Initialization

Here’s a quick side-by-side comparison to help you decide when to use eager or lazy initialization:

Feature Eager Initialization Lazy Initialization
Instance Creation At class loading When needed
Thread Safety Inherently thread-safe Requires synchronization
Resource Efficiency May waste resources if unused Efficient, only created when necessary
Complexity Simple and straightforward More complex, requires careful handling

Pros and cons of the singleton pattern

In general, a singleton does exactly what is expected of it:
  1. It guarantees that there will only ever be one instance of the class.

  2. It provides a single point of global access to that instance.

However, this pattern has shortcomings:
  1. A singleton violates the single responsibility principle: in addition to its direct duties, the singleton class also controls the number of instances.

  2. An ordinary class's dependence on a singleton is not visible in the class's public contract.

  3. Global variables are bad. Ultimately, a singleton turns into a hefty global variable.

  4. The presence of a singleton reduces the testability of the application as a whole and the classes that use the singleton in particular.

And that's it! :) We've explored the Java Singleton Class with you. Now, for the rest of your life, when conversing with your programmer friends, you can mention not only how good the pattern is, but also a few words about what makes it bad. Good luck mastering this new knowledge.

So far, you've nailed the basics of the Singleton pattern. But wait—did you know that sneaky tricks like reflection and serialization can actually break your Singleton?

Don’t worry! I’m here to show you exactly how these issues happen and, more importantly, how to stop them. Let’s dive in!

How Reflection Can Break Singleton (and How to Prevent It)

Reflection in Java allows you to inspect and modify classes at runtime. Sounds cool, right? But it can be dangerous for Singleton. Here’s why:

Even if you make your constructor private, reflection can still access it and create multiple instances.

Reflection in Action

import java.lang.reflect.Constructor;

public class ReflectionBreaksSingleton {
    public static void main(String[] args) {
        try {
            Singleton instance1 = Singleton.getInstance();

            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);  // Bypassing private constructor
            Singleton instance2 = constructor.newInstance();

            System.out.println(instance1 == instance2);  // Output: false

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        System.out.println("Singleton instance created!");
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Oops! Reflection just created a second instance! Not good.

Preventing Reflection Attacks

One simple trick is to throw an exception if someone tries to create another instance. Let’s fix it:

class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private static boolean instanceCreated = false;

    private Singleton() {
        if (instanceCreated) {
            throw new RuntimeException("Use getInstance() to create the Singleton!");
        }
        instanceCreated = true;
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Now, even reflection can’t sneak past this guard!

How Serialization Can Break Singleton (and How to Fix It)

Serialization lets us save and restore objects. But here’s the catch: deserialization creates a new object, breaking the Singleton rule.

Serialization Problem Example

import java.io.*;

public class SerializationBreaksSingleton {
    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();

        // Serialize
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        oos.writeObject(instance1);
        oos.close();

        // Deserialize
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
        Singleton instance2 = (Singleton) ois.readObject();
        ois.close();

        System.out.println(instance1 == instance2);  // Output: false
    }
}

class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Yikes! Serialization just broke our Singleton. Let’s fix that.

Fixing with readResolve()

By adding a readResolve() method, we can ensure deserialization returns the same instance.

class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    // Fix for serialization
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

Problem solved! Now deserialization will always return the original Singleton instance.

Reflection + Serialization: A Double Threat!

Reflection and serialization are like the dynamic duo of destruction when it comes to breaking Singleton. But don’t worry—we can outsmart them.

The Ultimate Protection: Enum Singleton

Want a bulletproof Singleton? Use an enum. It’s immune to both reflection and serialization!

enum Singleton {
    INSTANCE;

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

Usage:

public class Main {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.showMessage();
    }
}

Why is this awesome?

  • **Safe from Reflection:** Java prevents reflection from creating enum instances.
  • **Safe from Serialization:** Enums handle serialization internally. No readResolve() needed.
  • **Simple and Clean:** Less code, fewer problems.

Additional reading: