一、单例模式实现
1. 线程不安全版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Singleton { public: static Singleton* getInstance() { if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; } private: Singleton() = default; public: Singleton(const Singleton& other) = delete; Singleton& operator=(const Singleton& other) = delete; private: static Singleton* m_instance; };
|
这是最经典的实现方式,采用的是懒初始化的方式,但是在多线程的情况下,这种方式是不安全的。因为 m_instance = new Singleton()
不是一个原子操作,有可能在一个线程 new Singleton()
,还没有赋值给 m_instance
变量时,另外一个线程过来发现 m_instance
是 nullptr
,又去初始化了。这就导致产生两个对象。
2. 加锁的版本
1 2 3 4 5 6 7
| static Singleton* getInstance() { std::lock_guard<std::mutex> lck(mtx); if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
|
加锁之后,保证了线程安全,但是每次获取对象的时候,都需要加锁,导致存在性能问题。
3. 双检查锁(指令重排问题)
1 2 3 4 5 6 7 8 9
| static Singleton* getInstance() { if (m_instance == nullptr) { std::lock_guard<std::mutex> lck(mtx); if (m_instance == nullptr) { m_instance = new Singleton(); } } return m_instance; }
|
如上的 double-checked locking
双检查锁的思想,看起来很棒,只有在第一次需要的时候才会使用锁。但是直到 2000 年的时候才发现漏洞。因为内存读写的乱序执行(编译器的问题)(指令重排)
m_instance = new Singleton()
这句话会被分成三个步骤来执行:
- 分配了一个 Singleton 类型对象所需要的内存
- 在分配的内存处构造 Singleton 类型的对象
- 把分配的内存的地址赋给指针
m_instance
实际上,上面的这三个步骤中,步骤 2 和步骤 3 不一定是按序执行的。如果某个线程执行 m_instance = new Sinleton()
的时候按照步骤 1、3、2 的顺序执行,当执行完步骤 3 ,给 Sinelton 类型变量对象分配了内存并赋给了 m_instance
变量,此时 m_instance
不为 nullptr,但没有初始化。此时,其他线程执行 getInstance()
得到一个对象,但这个对象并没有真正的被构造。就会出现 bug。
4. 解决指令重排问题
Java 和 c# 发现了指令重排问题后,就加了一个关键字 volatile,在声明 m_instance
变量的时候,加上 volatile 修饰,编译器就会按照顺序执行(一定先分配内存、在执行构造、都完成之后再赋值)。到了 c++11 版本,终于有了内存栅栏这样的机制帮助我们解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Singleton* getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton(); std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed); } } return tmp; }
|
5. linux 平台的 pthread_once 函数
在 linux 中,pthread_once()
函数可以保证某个函数只执行一次。
1
| int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
|
这个函数使用初始值为 PTHREAD_ONCE_INIT
的 once_control
变量保证 init_routine()
函数在本进程执行序列中仅执行一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Singleton { public: static Singleton* getInstance() { pthread_once(&m_once, &Singleton::init); return m_instance; } private: Singleton() = default; static void init() { m_instance = new Singleton(); } public: Singleton(const Singleton& other) = delete; Singleton& operator=(const Singleton& other) = delete; private: static pthread_once_t m_once; static Singleton* m_instance; };
pthread_once_t Singleton::m_once = PTHREAD_ONCE_INIT; Singleton* Singleton::m_instance = nullptr;
|
6. c++11 版本简洁的跨平台方案
如上,例子 4 的方案有点麻烦,例子 5 的方案不能跨平台。c++11 已经为我们提供了 std::call_once
方法来保证函数在多线程环境中只被调用一次,而且支持跨平台。而且 c++ 的局部静态变量不仅只会初始化一次,而且还是线程安全的
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Singleton { public: static Singleton& getInstance() { static Singleton m_instance; return m_instance; } private: Singleton() = default; public: Singleton(const Singleton& other) = delete; Singleton& operator=(const Singleton& other) = delete; };
|
如上写法比较简洁,但是注意:
- gcc 4.0 之后的编译器才支持这种写法
- c++11 之前不能这么写
大佬关于单例模式的看法:https://www.cnblogs.com/loveis715/archive/2012/07/18/2598409.html