C++:谈谈单例模式的多种实现形式
笔者主要是想借本文介绍一下 C++ 11 静态局部变量的 magic static 特性。
单例模式:保证一个类仅有一个实例,并提供一个该实例的全局访问点。
- 稳定点:类只有一个实例,提供全局
- 变化点:有多个类都是单例,能否复用代码
实现 1:静态成员
- 构造函数和析构函数私有化
- 禁掉拷贝构造、拷贝赋值、移动构造、移动赋值
- 静态成员函数
- 静态私有成员
前两点针对唯一实例,后两点针对全局访问。
问题:当程序结束后,不会调用析构函数,堆上资源无法释放,内存泄漏。虽然程序结束后,堆上所有的数据被销毁,但是无法保存需要持久化的数据。
class Singleton {
public:
// 静态成员函数:全局访问点
static Singleton* getInstance() {
if(nullptr == _pInstance) {
_pInstance = new Singleton();
return _pInstance;
private:
// 构造函数和析构函数私有化
Singleton();
~Singleton();
// 禁掉拷贝构造、拷贝赋值、移动构造、移动赋值
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
private:
// 静态成员:静态成员函数只能访问静态成员
static Singleton* _pInstance;
// 静态成员需要初始化
Singleton* Singleton::_instance = nullptr;
实现 2:atexit + 懒汉模式
atexit 函数:在进程结束后利用回调函数自动释放堆空间。
/*
功能:注册给定函数,并在进程结束后调用该函数
参数:函数指针,指向被调用的函数(返回值、参数均为void)
#include <stdlib.h>
int atexit(void (*function)(void));
利用这一特性,在进程结束时 atexit 函数调用销毁函数,完成析构工作。
问题:atexit 函数本身安全,但是多线程环境下。存在线程安全问题。
static Singleton* getInstance() {
if(nullptr == _pInstance) {
// 问题:多个并发线程可能同时创建对象
_pInstance = new Singleton();
atexit(Singleton::Destructor);
return _pInstance;
为保证线程安全,需要加锁。对于加锁操作,只有第一次写操作创建对象的时候,需要加锁;其他时候都是读操作,没有必要加锁。因此这里在实现的时候可以采用双重检测
double check
的技巧。
#include <stdlib.h>
class Singleton {
public:
static Singleton* getInstance() {
// 线程安全,双重检测:double check
if (nullptr == _pInstance) {
std::lock_guard<std::mutex> lock(_mutex);
if (nullptr == _pInstance) {
// 问题:多线程环境下,cpu reorder
_pInstance = new Singleton();
// 注册回调函数,进程结束后,调用销毁函数
atexit(Singleton::Destructor);
return _pInstance;
private:
Singleton();
~Singleton();
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
// 注册销毁函数为atexit的回调函数,用于在进程结束后释放堆空间
static void Destructor() {
if (nullptr != _instance) {
delete _instance;
_instance = nullptr;
private:
static Singleton* _pInstance;
Singleton* Singleton::_instance = nullptr;
问题:new 操作符指令重排
C++ 98 表达单线程语义。而在多核多线程的情况下,若 cpu 指令重排,例如:对于 new 运算符的指令执行:分配内存、调用构造函数、返回指针。若发生 cpu 指令重排,会优化为分配内存、返回指针,却还没有调用构造函数初始化数据。此时,若有其他线程访问,可能造成程序的崩溃。
实现 3:原子变量 + 懒汉模式
C++ 11:多线程语义,cpu 指令重排,提供同步原语:原子变量、内存屏障等
原子变量解决
- 原子性问题
- 可见性问题:load 可以看见其他线程最新操作的数据, store 修改数据让其他线程可见
-
执行序问题:
memory_order_acuire
不能重排指令,memory_order_release
松散指令,可以重排指令。
内存屏障(内存栅栏)解决
- 可见性问题
- 执行序问题
使用原子变量解决原子性、可见性、执行序
class Singleton {
public:
static Singleton * GetInstance() {
Singleton* tmp = _instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
tmp = new Singleton;
_instance.store(tmp, memory_order_release);
atexit(Destructor);
return tmp;
static std::atomic<Singleton*> _instance;
static std::mutex _mutex;
std::atomic<Singleton*> Singleton::_instance; // 静态成员需要初始化
std::mutex Singleton::_mutex; // 互斥锁初始化
改进:若构造函数中存在其他原子性操作,则可以使用松散的指令执行方式,提升运行速度。使用内存屏障,避免 tmp 指针在 new 操作未执行完就返回给用户。
- 原子变量解决:原子性、可见性
- 内存栅栏解决:执行序
class Singleton {
public:
static Singleton * GetInstance() {
Singleton* tmp = _instance.load(std::memory_order_relaxed);
// 获取内存屏障
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
// 释放内存屏障
std::atomic_thread_fence(std::memory_order_release);
_instance.store(tmp, std::memory_order_relaxed);
atexit(Destructor);
return tmp;
static std::atomic<Singleton*> _instance;
static std::mutex _mutex;
std::atomic<Singleton*> Singleton::_instance; // 静态成员需要初始化
std::mutex Singleton::_mutex; // 互斥锁初始化
问题:代码复杂,书写困难。
实现4:atexit + 饿汉模式
懒汉模式是延迟加载,饿汉模式是提前加载。当系统开始运行,加载类的时候就初始化类实例,其他线程无法再创建实例,实现线程安全。
class Singleton {
public:
static Singleton* Singleton::getInstance() {
if(nullptr == _pInstance) {
_pInstance = new Singleton();
atexit(Singleton::Destructor);
return _pInstance;
// 全局初始化,使其在进程创建之前就不为空,防止子进程创建对象
Singleton* Singleton::_instance = getInstance();
问题:无论是否需要该类实例,都必须提前创建。
* 实现5:magic static
源自:C++ effective,C++ 11 magic static 特性,参考官方文档: 静态局部变量 static / thread local ,推荐使用。
- 静态局部变量在初始化的时候,并发线程同时进入声明语句,并发线程会阻塞等待其初始化结束。线程安全。
- 静态局部变量首次经过它的声明才会被初始化,在其后所有的调用中,声明都会被跳过。
因此,使用定义在栈上的局部静态变量保存单例对象,具备所有优点:
- 延迟加载
- 系统自动调用析构函数,回收内存
- 没有 new 操作带来的 cpu reorder 操作
- 线程安全
class Singleton {
public:
static Singleton& GetInstance() {
// magic static
// 定义在栈上的局部静态变量,进程结束后自动释放
static Singleton instance;
return instance;
private:
Singleton();
~Singleton();