thread-safe
singleton pattern
design patterns
concurrency
software development

Thread Safe singleton class

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

A singleton is only useful if it really stays single, even when multiple threads reach it at the same time. That is why thread safety matters: a naive lazy singleton can create multiple instances under load unless initialization is guarded correctly.

The Naive Lazy Singleton Is Not Safe

Consider this Java example:

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

This looks fine in single-threaded code, but it is not thread-safe. Two threads can both see instance == null and both create a different object.

That violates the pattern immediately.

Synchronized Access Works But Adds Contention

The simplest safe fix is to synchronize the accessor:

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

This is correct, but every call pays synchronization cost even after initialization is complete.

In many applications that overhead is acceptable, especially if the singleton is not accessed constantly. Still, Java offers cleaner alternatives.

The Initialization-On-Demand Holder Idiom

One of the best thread-safe lazy patterns in Java is the holder idiom:

java
1public class ConfigService {
2
3    private ConfigService() {
4    }
5
6    private static class Holder {
7        private static final ConfigService INSTANCE = new ConfigService();
8    }
9
10    public static ConfigService getInstance() {
11        return Holder.INSTANCE;
12    }
13}

Why this works:

  • class loading in Java is thread-safe
  • the nested holder class is not initialized until it is first used
  • no explicit synchronization is needed on normal access

This gives you lazy initialization without repeated locking.

Double-Checked Locking Requires volatile

Another common approach is double-checked locking:

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

This avoids locking after initialization, but it must use volatile. Without volatile, instruction reordering can expose a partially constructed object to another thread.

This pattern is correct in modern Java, but it is easier to get wrong than the holder idiom.

The Enum Singleton Is Even Simpler

If lazy loading is not required in a special way, an enum singleton is a very strong option:

java
1public enum ConfigService {
2    INSTANCE;
3
4    public String read(String key) {
5        return "value-for-" + key;
6    }
7}

Usage:

java
System.out.println(ConfigService.INSTANCE.read("mode"));

This approach is:

  • thread-safe
  • serialization-safe
  • resistant to many reflection-related singleton breakage patterns

That is why many Java developers consider it the most robust singleton implementation when the style fits the use case.

Pick Based On What You Actually Need

A practical ranking for Java is often:

  1. enum singleton when a singleton is truly appropriate
  2. holder idiom for lazy initialization in a normal class
  3. synchronized accessor if simplicity matters more than contention
  4. double-checked locking only when you understand why it works

The bigger design question is whether you need a singleton at all. Many singletons are really just global mutable state in disguise, which makes testing and dependency management harder.

If dependency injection can manage the lifetime for you, that is often a cleaner choice.

Common Pitfalls

The biggest mistake is writing a lazy singleton with no synchronization and assuming "it probably will not race." Under concurrency, it absolutely can.

Another mistake is using double-checked locking without volatile. That pattern is not safely correct otherwise.

People also use singleton as a default architecture tool when a regular injected service would be easier to test and maintain.

Finally, thread safety at initialization time does not automatically make the singleton's internal mutable state thread-safe. The instance may be unique, but its methods can still need synchronization.

Summary

  • A lazy singleton is not thread-safe unless initialization is protected.
  • A synchronized accessor is correct but may add unnecessary contention.
  • The holder idiom is a strong Java default for lazy thread-safe initialization.
  • Enum singletons are simple and robust when their style fits the design.
  • Even a thread-safe singleton can still expose unsafe mutable state internally if you are not careful.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.