Loading learning content...
Armed with our understanding of memory models, we can now implement Double-Checked Locking correctly—and explore alternative patterns that achieve the same goal with different tradeoffs.
This page serves as a reference for production-quality implementations. We'll cover:
Each implementation includes commentary on why it works and common mistakes to avoid.
By the end of this page, you will have production-ready implementations of thread-safe lazy initialization for multiple languages, understand the tradeoffs between different approaches, and know which pattern to choose for your specific use case.
Java 5.0 and later provides all the guarantees needed for correct DCL. The key requirement is the volatile keyword on the instance field.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
/** * Production-Ready Double-Checked Locking in Java * * Requirements: * - Java 5.0 or later (JSR-133 memory model) * - volatile keyword on instance field (ESSENTIAL) * - Local variable optimization (optional but recommended) */public class DatabaseConnectionManager { // ═══════════════════════════════════════════════════════════════ // CRITICAL: volatile keyword provides happens-before guarantee // ═══════════════════════════════════════════════════════════════ private static volatile DatabaseConnectionManager instance = null; // Instance fields private final ConnectionPool pool; private final Configuration config; /** * Private constructor - prevents external instantiation */ private DatabaseConnectionManager() { // Expensive initialization this.config = loadConfiguration(); this.pool = new ConnectionPool(config.getPoolSize()); pool.initialize(); System.out.println("DatabaseConnectionManager initialized"); } private Configuration loadConfiguration() { // Simulate loading from external source return new Configuration(); } /** * Thread-safe lazy getter using Double-Checked Locking * * Performance characteristics: * - First call: Synchronized (slow, happens once) * - Subsequent calls: Single volatile read (fast) */ public static DatabaseConnectionManager getInstance() { // ═══════════════════════════════════════════════════════════ // OPTIMIZATION: Local variable avoids multiple volatile reads // ═══════════════════════════════════════════════════════════ DatabaseConnectionManager localRef = instance; // First check: No synchronization // Volatile read provides visibility guarantee if (localRef == null) { // Only synchronize when instance might need creation synchronized (DatabaseConnectionManager.class) { // Read again inside synchronized block localRef = instance; // Second check: Another thread may have initialized if (localRef == null) { // Create and assign // The volatile write ensures all constructor // writes are visible before the reference becomes visible instance = localRef = new DatabaseConnectionManager(); } } } return localRef; } /** * Public methods to use the singleton */ public Connection getConnection() throws SQLException { return pool.borrowConnection(); } public void releaseConnection(Connection conn) { pool.returnConnection(conn); }} /** * Usage example */public class Application { public static void main(String[] args) { // First call - initializes the singleton DatabaseConnectionManager manager1 = DatabaseConnectionManager.getInstance(); // Subsequent calls - returns cached instance DatabaseConnectionManager manager2 = DatabaseConnectionManager.getInstance(); assert manager1 == manager2; // Same instance // Use the singleton try { Connection conn = manager1.getConnection(); // ... use connection ... manager1.releaseConnection(conn); } catch (SQLException e) { e.printStackTrace(); } }}The single most common mistake is omitting volatile. The code will appear to work in testing but fail in production under high concurrency. Code review should always verify that instance fields in DCL patterns are volatile.
The Initialization-on-Demand Holder idiom (also called the Bill Pugh Singleton) is often a better choice than DCL in Java. It leverages the JVM's class loading mechanism to achieve thread-safe lazy initialization without any synchronization in the access path.
How it works:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/** * Initialization-on-Demand Holder Idiom * * This is often PREFERRED over DCL in Java because: * - Simpler code (no volatile, no synchronized) * - Equally performant (no synchronization on fast path) * - Relies on JVM guarantees (less room for error) * - Works in all Java versions (not just 5+) */public class ConfigurationManager { // Private constructor private ConfigurationManager() { System.out.println("ConfigurationManager initialized"); // Expensive initialization here } /** * Inner static class - JVM guarantees: * 1. Not loaded until first reference * 2. Loaded exactly once * 3. Initialization is thread-safe */ private static class Holder { // Static initializer runs when Holder class is loaded // JVM guarantees this happens exactly once, thread-safely private static final ConfigurationManager INSTANCE = new ConfigurationManager(); } /** * Thread-safe lazy getter * * First call: Triggers loading of Holder class, which * initializes INSTANCE * Subsequent calls: Returns already-initialized INSTANCE * * No volatile reads, no lock acquisition - just return a constant */ public static ConfigurationManager getInstance() { return Holder.INSTANCE; } // Public methods public String getProperty(String key) { // ... return null; }} /** * Why this works (JVM Specification): * * Section 12.4.2 - "Class Initialization": * "The implementation of the Java Virtual Machine is responsible * for taking care of synchronization and recursive initialization * by using the following procedure..." * * The JVM holds an initialization lock while initializing a class. * Other threads attempting to use the class block until * initialization completes. This is exactly what we need! * * Benefits over DCL: * - No volatile (simpler, no overhead) * - No synchronized (no lock overhead) * - No room for implementation error (fewer moving parts) * - Works in Java 1.1+ (not dependent on fixed memory model) */For most Java singleton/lazy initialization cases, the Holder idiom is simpler and equally performant. Use DCL only when you have a specific reason the Holder idiom doesn't work (e.g., runtime-dependent initialization parameters).
Joshua Bloch (Effective Java) argues that enum singletons are the best way to implement singletons in Java. The JVM guarantees that enum values are instantiated exactly once, thread-safely, and serialization is handled correctly.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
/** * Enum Singleton - "The Right Way" per Joshua Bloch * * Advantages: * - Thread-safe by JVM guarantee * - Serialization handled correctly (no duplicates) * - Reflection attacks prevented (can't create new enum instances) * - Most concise syntax */public enum CacheManager { // The singleton instance - created when enum class is loaded INSTANCE; // Instance fields (initialized in constructor) private final Map<String, Object> cache; private final int maxSize; // Constructor (implicitly private for enums) CacheManager() { System.out.println("CacheManager initialized"); this.maxSize = 10000; this.cache = new ConcurrentHashMap<>(maxSize); } // Public methods public void put(String key, Object value) { if (cache.size() < maxSize) { cache.put(key, value); } } public Object get(String key) { return cache.get(key); } public void clear() { cache.clear(); }} /** * Usage */public class Application { public static void main(String[] args) { // Access the singleton CacheManager.INSTANCE.put("user:123", new User("Alice")); Object user = CacheManager.INSTANCE.get("user:123"); // All of these refer to the same instance: CacheManager c1 = CacheManager.INSTANCE; CacheManager c2 = CacheManager.INSTANCE; assert c1 == c2; // Always true }} /** * Why enum singletons are special: * * 1. Thread Safety * - JVM initializes enum values during class loading * - This is inherently thread-safe (like Holder idiom) * * 2. Serialization * - Enum serialization is handled specially by Java * - Deserializing always returns the existing INSTANCE * - No duplicate singletons from serialization attacks * * 3. Reflection Protection * - Java explicitly prohibits creating new enum instances via reflection * - Enum.class.newInstance() throws IllegalArgumentException * * 4. Lazy Loading (with a catch) * - Enum is loaded when any of its values are first accessed * - But if you have multiple enum values, ALL are initialized * - For single-instance enums like INSTANCE, this is effectively lazy */Enum singletons are initialized when the enum class is loaded—which happens when you first access the enum. This is lazy from the application's perspective, but all enum values in the same enum initialize together. For true per-value laziness, use Holder or DCL.
C++11 and later provides multiple correct approaches to thread-safe lazy initialization. Unlike pre-C++11, these are portable and standards-compliant.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/** * C++11 Static Local Variable - Magic Statics * * SIMPLEST and often BEST approach: * - C++11 guarantees thread-safe initialization of function-local statics * - Known as "Meyers Singleton" (Scott Meyers) * - Zero boilerplate */class EventBus {private: std::vector<std::function<void(const Event&)>> handlers; EventBus() { std::cout << "EventBus initialized" << std::endl; } public: EventBus(const EventBus&) = delete; EventBus& operator=(const EventBus&) = delete; /** * Thread-safe lazy getter using C++11 static local */ static EventBus& getInstance() { // C++11 guarantees this is initialized exactly once, // thread-safely, on first call static EventBus instance; return instance; } void subscribe(std::function<void(const Event&)> handler) { handlers.push_back(std::move(handler)); } void publish(const Event& event) { for (const auto& handler : handlers) { handler(event); } }}; // Usage - no boilerplate needed!void example() { EventBus::getInstance().subscribe([](const Event& e) { std::cout << "Received: " << e.name << std::endl; });} /** * Why this works (C++11 Standard, 6.7.4): * * "If control enters the declaration concurrently while * the variable is being initialized, the concurrent * execution shall wait for completion of the initialization." * * The compiler generates code equivalent to: * * static bool __guard = false; * static EventBus __instance; * * if (!__guard) { * // Thread-safe initialization with guard variable * // (actual implementation is more sophisticated) * } * return __instance; * * Note: Returns reference, not pointer - guarantees non-null */For C++11 and later, the static local (Meyers Singleton) approach is almost always the best choice. It's the simplest, has zero boilerplate, and is guaranteed thread-safe by the standard. Use DCL only when you need explicit pointer semantics or manual destruction control.
C# and .NET provide multiple approaches to thread-safe lazy initialization, including DCL and the built-in Lazy<T> type.
1234567891011121314151617181920212223242526272829303132333435363738
/** * Double-Checked Locking in C# * * Uses volatile for the instance reference * Note: C# volatile has slightly different semantics than Java */public sealed class DatabaseManager { // volatile is REQUIRED for correct DCL private static volatile DatabaseManager _instance; private static readonly object _lock = new object(); private readonly string _connectionString; private DatabaseManager() { Console.WriteLine("DatabaseManager initialized"); _connectionString = LoadConnectionString(); } private string LoadConnectionString() => "Server=localhost;Database=MyApp;"; public static DatabaseManager Instance { get { // First check (volatile read) if (_instance == null) { lock (_lock) { // Second check if (_instance == null) { _instance = new DatabaseManager(); } } } return _instance; } } public string GetConnection() => _connectionString;}Unless you have a specific reason to implement DCL manually, use Lazy<T>. It's tested, maintained by Microsoft, and expresses intent clearly. The LazyThreadSafetyMode options give you fine-grained control when needed.
Python has the Global Interpreter Lock (GIL), which makes many operations implicitly thread-safe. However, for true thread-safe lazy initialization, explicit locking is still recommended.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
"""Thread-Safe Singleton Implementations in Python"""import threadingfrom functools import lru_cachefrom typing import Optional class SingletonMeta(type): """ Thread-safe singleton metaclass using DCL pattern. Classes using this metaclass will be singletons. """ _instances: dict = {} _lock: threading.Lock = threading.Lock() def __call__(cls, *args, **kwargs): # First check (outside lock) if cls not in cls._instances: with cls._lock: # Second check (inside lock) if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class DatabasePool(metaclass=SingletonMeta): """Example singleton using the metaclass""" def __init__(self): print("DatabasePool initialized") self.connections = [] self._setup_pool() def _setup_pool(self): # Create initial connections for i in range(10): self.connections.append(f"connection_{i}") def get_connection(self): return self.connections[0] if self.connections else None # ═══════════════════════════════════════════════════════════════# Alternative: Module-level singleton (Python's natural approach)# ═══════════════════════════════════════════════════════════════ class _ConfigManager: """Private implementation class""" def __init__(self): print("ConfigManager initialized") self.settings = {} self._load_config() def _load_config(self): self.settings = {"debug": True, "log_level": "INFO"} def get(self, key: str): return self.settings.get(key) # Module-level instance - Python imports are cached# First import initializes; subsequent imports return cached moduleconfig_manager = _ConfigManager() # ═══════════════════════════════════════════════════════════════# Alternative: Decorator approach# ═══════════════════════════════════════════════════════════════ def singleton(cls): """ Decorator to make a class a thread-safe singleton. """ instances = {} lock = threading.Lock() def get_instance(*args, **kwargs): if cls not in instances: with lock: if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singletonclass EventBus: """Example singleton using decorator""" def __init__(self): print("EventBus initialized") self.subscribers = [] def subscribe(self, handler): self.subscribers.append(handler) def publish(self, event): for handler in self.subscribers: handler(event) # Usageif __name__ == "__main__": # All of these return the same instance pool1 = DatabasePool() pool2 = DatabasePool() print(f"Same pool: {pool1 is pool2}") # True bus1 = EventBus() bus2 = EventBus() print(f"Same bus: {bus1 is bus2}") # TrueWhile Python's GIL prevents true parallel execution of bytecode, it doesn't guarantee that check-then-act operations are atomic. A thread switch can occur between checking if the instance exists and creating it. Always use explicit locking for singletons.
With multiple correct approaches available, how do you choose? Here's a decision framework:
| Language | First Choice | Alternative | When to Use Alternative |
|---|---|---|---|
| Java | Holder Idiom | Enum Singleton | When serialization is a concern |
| Java | Holder Idiom | DCL with volatile | When init needs runtime params |
| C++ | Static Local (Meyers) | std::call_once | When you need manual destruction |
| C++ | Static Local (Meyers) | DCL with atomic | When need pointer semantics |
| C# | Lazy<T> | DCL with volatile | Rarely needed; Lazy<T> is flexible |
| Python | Module-level instance | Metaclass/Decorator | When class-based API is preferred |
Before implementing any singleton pattern, consider whether you actually need a singleton. Singletons introduce global state, make testing harder, and hide dependencies. Often, dependency injection provides the same "single instance" behavior with better testability.
We've covered production-ready implementations of thread-safe lazy initialization:
Congratulations!
You've now mastered the Double-Checked Locking pattern—from understanding why naive implementations fail, to the memory model foundations that make correct implementations possible, to production-ready code in multiple languages.
This knowledge extends beyond DCL to all concurrent programming. The memory model concepts, happens-before relationships, and synchronization primitives apply whenever threads share data.
You've completed the Double-Checked Locking module. You now understand one of the most notorious patterns in concurrent programming—why it fails when implemented incorrectly, and how to implement it safely when needed.