方式
- 友元
- 内部类 + 静态数据成员
- atexit
- pthread_once
- 饿汉模式 懒汉模式
- 锁
- Meyers’ Singleton
内部类 + 静态数据成员
#include <iostream>
using namespace std;
class Singleton {
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
//不管返回类型,只要函数签名(函数名,参数)不同就能删
class Deleter {
public:
~Deleter() {
if (Singleton::_instance) {
delete Singleton::_instance;
Singleton::_instance = nullptr;
}
}
};
public:
static Singleton &getinstance() { // 返回了一个Singleton类型对象的引用
static Deleter deleter;//创建一次静态对象,之后就不会再创建
//且在程序销毁时销毁deleter时,deleter会销毁_instance
if (!_instance) {
_instance = new Singleton();
}
return *_instance;
}
void print() { cout << _a << endl; }
private:
static Singleton *_instance;
int _a = 123;
};
Singleton *Singleton::_instance = nullptr;
int main() { Singleton::getinstance().print(); }
饿汉懒汉
饿汉一开始就会创建对象
懒汉第一次调用才创建对象,多线程情况下,不安全。
- 线程A还没创建完对象(
_instance = new Singleton()
),此时_instance
仍为nullptr - 这时候线程B也来创建对象了,if判断_instance仍是nullptr,所以会产生两个对象,程序就有问题了
锁
无脑锁的话代价高,if语句前先锁上,执行完创建再解锁,每次都要加与释放。
双检查锁
在if语句判断之后加锁,即判断为nullptr再加锁,就避免之后每次还加锁解锁了。
看似很完美,但有漏洞,因为内存读写的乱序执行(编译器的问题)
分析:_instance = new Singleton()这句话可以分成三个步骤来执行:
分配了一个Singleton类型对象所需要的内存。
在分配的内存处构造Singleton类型的对象。
把分配的内存的地址赋给指针_instance。
实际上只能确定步骤1是最先执行的 假如某个线程A是按照1,3,2的顺序执行,那么刚刚执行完步骤3给Singleton类型分配了内存(此时_instance就不是nullptr了)就切换到了线程B 由于_instance已经不是nullptr了,所以线程B会直接执行return *_instance得到一个对象,而这个对象并没有真正的被构造!!严重bug就这么发生了。
编译器可能会对代码进行调整优化,让那些看起来顺序不影响结果的代码进行顺序的调整,但是实际上在多线程中可能会有问题。
简单来说就是,可能会先分配内存,使_instance不为nullptr,但此时如果另一个线程也来创建单例对象,会发现不为nullptr,就会直接返回单例对象,但实际上该对象还没真正被创建。
c++11提供了原子操作解决该问题,感觉有点深,再说吧
ptrhead_once
在linux中,pthread_once()函数可以保证某个函数只执行一次。
Meyers’ Singleton
class Singleton {
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
使用局部静态变量来实现懒加载
c++11后可确保线程安全,可谓无敌。