September 1, 2026
Double-Checked Locking, The Pattern That Saves Performance

Double-Checked Locking, The Pattern That Saves Performance

In the early years of my career, when I first encountered double-checked locking in code, I thought it was a redundant code line checking same thing twice. I told myself why would anyone check the same condition twice? I asked a senior developer, and he explained how it ensure thread safety and save performance in concurrency. Actually, double-checked locking is a tiny but useful pattern that appears in concurrent programming.

What Is Double-Checked Locking

At its core, double-checked locking is a technique that reduces the overhead of acquiring a lock by first checking the condition without synchronization/locking in concurrent environment. Only if the condition appears to be true, we acquire the lock and check again. Its use cases are lazy singleton initialization, expensive resource initialization, optional application features, and memorized shared state .

Here’s the canonical structure:

def get_resource():
    if resource is None:              # First check (no lock)
        with lock:                    # Acquire lock
            if resource is None:      # Second check (with lock)
                resource = create()   # Expensive operation
    return resource

But let’s step back and understand why this pattern exists.

Locks Are Expensive

In concurrent programming, locks are essential but costly. Every time you acquire and release a lock, you’re:

  • Paying for system-level synchronization
  • Potentially blocking other threads/coroutines
  • Introducing contention in your application

Consider a web cache that stores expensive computation results:

class Cache:
    def __init__(self):
        self._lock = threading.Lock()
        self._heavy_data = None

    def get_data(self):
        with self._lock:  # Every single request acquires the lock!
            if self._heavy_data is None:
                self._heavy_data = self._compute_heavy_data()
            return self._heavy_data

In a high-traffic system, this lock becomes a bottleneck. Thousands of requests per second all waiting for the same lock, even after the data is cached.

The Pattern in Action

Double-checked locking shines in scenarios where:

  1. An operation is expensive to perform
  2. The operation should be performed only once
  3. The result is accessed frequently
  4. The system is multi-threaded

Let’s look at some real-world applications:

1. Lazy Initialization of Resources

class ConnectionPool:
    _instance = None
    _lock = threading.Lock()

    def get_connection(self):
        if self._connection is None:        # Quick check
            with self._lock:                # Only lock if needed
                if self._connection is None: # Double-check
                    self._connection = self._create_connection()
                    self._pool = self._initialize_pool()
        return self._connection

2. Expensive Computations

class DataProcessor:
    _cache = None
    _lock = threading.Lock()

    def get_processed_data(self):
        if self._cache is None:                # Check without lock
            with self._lock:                   # Acquire lock
                if self._cache is None:        # Check with lock
                    raw = self._load_massive_dataset()
                    self._cache = self._process(raw)
        return self._cache

3. Lazy Loading in Web Applications

class ConfigManager:
    _config = None
    _lock = threading.Lock()

    def get_config(self):
        if self._config is None:
            with self._lock:
                if self._config is None:
                    self._config = self._load_config_from_file()
                    self._config = self._parse_and_validate()
        return self._config

The Performance Impact: Let’s Talk Numbers

I ran some benchmarks to illustrate the difference. Here’s what I found with 10,000 concurrent requests:

PatternTime (ms)Operations/sec
Always lock78.94126,675
Double-checked11.16895,873

You can find benchmark script here : https://gist.github.com/birddevelper/21793b07679680123942130d46bc8bd3. I also added benchmark for using double-checked lock in class __new__ method vs in meta-class __call__ which is itself another long story.

As you can see, double-checked locking is ~7x faster than always locking! Once the resource is initialized, the first check fails and we bypass the lock entirely. Only the first few threads pay the synchronization cost.

Let’s check a Java example:

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {           // First check: avoid locking after init
            synchronized (Singleton.class) {
                if (instance == null) {   // Second check: only one initializer
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

The second check prevents multiple initialization after threads enter the synchronized block, while synchronized and volatile provide the memory-visibility and ordering guarantees. When a thread sees a non-null volatile reference, it also sees the effects of the constructor that happened-before that write.

Let me know in the comments, I’d love to hear your experience with this pattern!

One thought on “Double-Checked Locking, The Pattern That Saves Performance

  1. I remember feeling similarly confused at first, it’s definitely a subtle but important optimization.

Leave a Reply

Your email address will not be published. Required fields are marked *