The Singleton Pattern and the Race Two Threads Can Win
The classic lazy singleton has a bug that never shows up on one thread: two threads can both pass the null check at once and build two separate instances.
The last post built a singleton with a private constructor and a getInstance() method that builds the instance the first time, then reuses it forever after.
Here's the bug, in one sentence: two threads can both see an empty instance at the exact same moment, and both build their own, so the pattern's entire promise, only one instance, silently breaks.
Where the Naive Version Breaks
Look at getInstance() again. It does two separate things: check if the instance exists, and if not, create one.
class Singleton {
public:
static Singleton* getInstance() {
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
};
Singleton* Singleton::instance_ = nullptr;On a single thread, "check, then create" always happens in order, no problem. But picture two threads calling getInstance() at nearly the same instant, right when the program starts.
Thread A checks instance_ == nullptr. It's true, so A starts building a Singleton. But before A finishes and writes the result into instance_, thread B also checks instance_ == nullptr. It's still true, because A hasn't written anything yet. B now builds its own, completely separate Singleton, and neither thread knows the other one exists.
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> threads;
std::vector<Singleton*> results(4);
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&results, i]() {
results[i] = Singleton::getInstance();
});
}
for (auto& t : threads) t.join();
for (Singleton* s : results) {
std::cout << s << "\n";
}
return 0;
}Run this enough times, and some runs print four identical addresses, and some runs don't. That inconsistency is the bug. Two unsynchronized threads both reading and writing instance_ at once isn't just "sometimes wrong," it's undefined behavior in C++, whether or not a given run happens to show it.
Fixing It With a Mutex
The obvious fix: let only one thread run "check, then create" at a time.
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;std::lock_guard grabs mutex_ the moment getInstance() starts, and releases it automatically when the function returns. While one thread holds the lock, every other thread calling getInstance() has to wait its turn. No two threads can ever be inside the "check, then create" step at the same time again.
This works, but it's wasteful. The lock only actually matters the very first time, while the instance is being built. Every call after that just needs to read a pointer that's already set, yet this version still locks the mutex on every single call, forever.
The Better Fix: A Function-Local Static
C++ has a cleaner answer, and it doesn't need a mutex written by hand at all.
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};Since C++11, the standard guarantees that a static variable declared inside a function is initialized exactly once, even if multiple threads call that function at the same time. The compiler generates the locking for you, but only around that one-time initialization, not around every call. After the first call, reading instance is just a normal variable access, no lock, no branch, no waiting.
This version is known as a Meyers' Singleton, and it's the idiomatic way to write this in modern C++: shorter than the mutex version, and faster after the first call.
What TypeScript, JavaScript, and Java Do Differently
TypeScript and JavaScript never have this race at all. Both run on a single thread, so two pieces of your code are never literally executing at the same instant, there's no window for two calls to overlap. A plain module-level singleton, or the same getInstance() shape from the last post, is already safe.
class Singleton {
private static instance: Singleton | null = null;
private constructor() {}
static getInstance(): Singleton {
if (Singleton.instance === null) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}Java has its own version of the function-local static trick, built on how the JVM loads classes.
class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}Holder only gets loaded, and its INSTANCE only gets created, the first time getInstance() actually runs. The JVM already guarantees class loading is thread-safe, so this gets the exact same benefit as C++'s function-local static: lazy, safe, and no manual lock.
g++ -std=c++17 -pthread main.cpp -o main && ./main (C++) or npm start (TypeScript) to see it run.Try It Yourself
A ConnectionPool singleton hands out database connections. At startup, several worker threads all try to grab it at once, before anything else in the app has run.
- Build the naive lazy version from the top of this post, with
ConnectionPoolin place ofSingleton. - Spawn at least four threads that all call
getInstance()immediately at startup, and print the address each one gets back. - Run it several times. Try to catch a run where the addresses don't all match.
- Fix it with a function-local static, then confirm every run now prints the same address, every time.
The Essentials
- The naive lazy singleton has a real race: two threads can both pass the null check before either one finishes building the instance, producing two separate objects.
- A
std::mutexfixes it, but pays a locking cost on every single call, forever, even after the instance already exists. - A function-local
static(C++11+) is the idiomatic fix: the compiler guarantees thread-safe one-time initialization, with no lock on any call after the first.
Keep reading