Last updated:2026-10-12
C++ Singleton Pattern¶
0. Introduction¶
Nobody escapes design-pattern questions in interviews. This section covers the most frequently asked design pattern in interviews — the singleton pattern — looking at classification and thread safety, from both a pre-C++11 and a C++11 perspective, and at which thread-safe singleton solutions exist. Read this article carefully and you will never have to worry about it in interviews again.
1. The Well-Known Singleton¶
In typical books, or as most people know it, the singleton pattern looks like this:
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
if (p == nullptr)
p = new singleton();
return p;
}
This is a very simple implementation: the constructor is declared private or protected to prevent external functions from instantiating the class, an internal static class pointer holds the unique instance, and the instance itself is provided by a public method that returns the class's sole instance.
Of course, this code is only suitable for a single thread; with multiple threads it is unsafe. Consider two threads simultaneously calling instance for the first time and both detecting that p is nullptr: both threads would construct an instance for p, which violates the singleton rule.
2. Lazy and Eager Initialization¶
Singletons come in two flavors:
- Lazy
- The instance is not created until the first time it is needed; the code above is the lazy implementation.
- Eager
- The instance is created at the moment the singleton class is defined.
Here is the eager implementation as well:
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = new singleton();
singleton* singleton::instance() {
return p;
}
This one is of course thread-safe. The thread-unsafe variant we usually talk about is the lazy one; below we will go through thread-safety optimizations of the lazy implementation.
3. Locking for Multithreading¶
In C++ there is a class for locking whose implementation principle is RAII, so you never have to unlock manually: lock_guard. We use it here to take the lock.
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
return p;
}
With this version, the situation above — two threads both executing inside the p == nullptr branch — cannot happen. While thread A executes p = new Singleton(), if thread B calls instance(), B will block at the lock and wait until thread A finishes and releases it. Hence it is thread-safe.
But this version performs very poorly, because every call to instance() acquires and releases the lock, while that step is only necessary for the first new Singleton(). Once p has been created, no matter how many threads access it concurrently, checking with if (p == nullptr) is sufficient (it is only a read, no lock needed) and there is no thread-safety problem; adding a lock instead introduces a performance problem.
Hence DCL.
4. Double-Checked Locking Pattern¶
The version above takes the lock and releases it unconditionally, while reads pose no thread-safety problem at all — so locking is only needed when the instance is first created, and never afterwards. First, here is the DCLP implementation:
singleton* singleton::instance() {
if(p == nullptr) { // 第一次检查
Lock lock;
if(p == nullptr){ // 第二次检查
p = new singleton;
}
}
return p;
}
Building on the above, we can write double-checked locking + automatic reclamation:
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if(singleton::p)
delete singleton::p;
}
};
static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
std::mutex singleton::lock_;
singleton* singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
}
return p;
}
The key to DCLP is that most calls to instance will see p as non-null and thus not even attempt to initialize it. Therefore, DCLP checks whether p is null before trying to acquire the lock. Only when that check succeeds (that is, p has not yet been initialized) does it take the lock, and then it checks again whether p is still null (hence the name double-checked locking). The second check is necessary because, as we have just seen, another thread may well have initialized p after our first check but before we successfully acquire the lock.
The code above looks wonderful, yet quite a long time passed before the flaw was discovered. The reason: out-of-order execution of memory reads and writes (a compiler problem).
Consider again the line that initializes p:
This statement causes three things to happen:
- Allocate memory able to store a
singletonobject; - Construct a
singletonobject in the allocated memory; - Make
ppoint to the allocated memory.
You might assume these three steps run in order, but in fact only step 1 is guaranteed to run first; steps 2 and 3 are not. There lies the problem.
- Thread A calls instance, performs the first test of p, acquires the lock, executes steps 1 and 3, and is then suspended. At this point p is non-null, but the memory p points to does not yet contain a constructed Singleton object.
- Thread B calls instance, sees that p is non-null, and returns it to instance's caller. The caller dereferences the pointer to get a singleton — oh no, an object that has not been constructed yet. There is the bug.
DCLP works correctly only if steps one and two are executed before step three, but there is no way to express such a constraint in C or C++. It is like a dagger thrust into DCLP's heart: we need to place constraints on the relative order of instructions, but our language gives us no way to express them.
5. The memory barrier Instruction¶
In C++11, this DCLP problem was solved.
Because the new C++11 standard specifies a new memory model that guarantees no thread switch happens while the three steps above execute — the initialization process is effectively "atomic" — DCL can be used correctly again. However, under C++11 there is an even more concise way to write a multithreaded singleton, which we will introduce later.
The pre-C++11 solution is the barrier instruction. To make it execute correctly, a memory barrier must be inserted between steps 2 and 3, forcing the CPU to run in the order 1, 2, 3.
First implementation:
Based on operator new + placement new, writing the code following the 1, 2, 3 order.
// method 1 operator new + placement new
singleton *instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr) {
singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
new(tmp)singleton();
p = tmp;
}
}
return p;
}
Second implementation:
Based on directly embedding the ASM instruction mfence; Unix's barrier macro is also implemented with this instruction.
#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
barrier();
if (p == nullptr) {
p = new singleton();
}
}
return p;
}
What is usually done is to call an instruction provided by the CPU whose effect is to prevent the CPU from moving instructions that come before it to after it; such an instruction is commonly called a barrier. In the code above, asm indicates that this is an assembly instruction; volatile is optional — if used, it tells the compiler that this assembly instruction must not be optimized. lwsync is the barrier instruction provided by POWERPC.
6. Static Local Variables¶
Scott Meyers proposed a concise singleton form in Effective C++:
- In a single thread, correct.
- In C++11 and later versions (such as C++14) with multiple threads, correct.
- Before C++11 with multiple threads, not necessarily correct.
The reason is that the standards before C++11 did not specify a memory model for local static variables, so it was not thread-safe. In C++11, however, it is thread-safe, because the new C++ standard stipulates that while one thread is initializing a variable, other threads must wait until that initialization completes before they may access it.
The memory orders used above:
- memory_order_relaxed: the relaxed memory order; it only guarantees that operations on the atomic object are atomic
- memory_order_acquire: an acquire operation; when reading an atomic object, no later reads or writes in the current thread may be reordered before this operation, and all memory writes performed by other threads before releasing the same atomic object become visible to the current thread
- memory_order_release: a release operation; when writing an atomic object, no earlier reads or writes in the current thread may be reordered after this operation, and all memory writes of the current thread become visible to other threads that acquire the same atomic object
7. Atomic¶
Before C++11, besides implementing a thread-safe singleton with locks, you could also use the atomic operations built into each compiler.
When java and c# discovered the reordering problem, they added the keyword volatile; when declaring the variable p you must add the volatile modifier, and once the compiler sees it, it knows that reordering is not allowed there (memory must be allocated first, then the constructor executed, and only after both complete may the assignment happen).
The c++ standard, however, never fixed this, so VC++ added this keyword in its 2005 release, but it is not portable (it only supports Microsoft platforms).
Then came c++ 11, which introduced a multithreading-friendly memory model to eliminate these flaws at the root. At last we have a mechanism that helps us implement a portable solution.
mutex singleton::lock_;
atomic<singleton *> singleton::p;
/*
* std::atomic_thread_fence(std::memory_order_acquire);
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
singleton *tmp = p.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if (tmp == nullptr) {
lock_guard<mutex> guard(lock_);
tmp = p.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new singleton();
atomic_thread_fence(memory_order_release);
p.store(tmp, memory_order_relaxed);
}
}
return p;
}
Note that the code above uses two key terms, acquire and release:
- Acquire is a read operation on memory; no later reads or writes in the current thread may be reordered before it.
- Release is a write operation on memory; no earlier reads or writes in the current thread may be reordered after it.
acquire and release usually appear in pairs, in order to guarantee that if a release on the same atomic object happens before an acquire, all memory modifications made before the release are fully visible to memory reads after the acquire.
8. pthread_once¶
On Unix platforms, besides atomic operations, if C++11 is not an option, a singleton can also be implemented with pthread_once.
The prototype is as follows:
Implementation:
class singleton {
private:
singleton(); //私有构造函数,不允许使用者自己生成对象
singleton(const singleton &other);
//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
static void init() {
p = new singleton();
}
static pthread_once_t ponce_;
static singleton *p; //静态成员变量
public:
singleton *instance() {
// init函数只会执行一次
pthread_once(&ponce_, &singleton::init);
return p;
}
};
9. Summary¶
This article covered several kinds of singletons, thread-safe singletons in particular, as well as several thread-safe implementations that do not use C++11: the memory barrier, static local variables, the pthread_once approach, C++11's atomic implementation, and more.
Finally, note that for the destructor of the singleton classes above, refer to the double-checked locking + automatic reclamation implementation: you must declare a static local variable inside the class. A static local variable can be thought of as a global variable; when the program ends, its destructor is invoked automatically. That is why a nested class is declared and defined inside the class instead of writing the singleton's destructor directly.
10. References¶
https://thorns.cn/2019/03/09/DCLP.html