所谓懒汉,讲的是对象的构造是在第一次调用获取对象接口时才进行构造的
class Singleton{
public:
//非可重入函数
static Singleton*GetInstance(){
if(instance == nullptr){
instance = new Singleton();
}
return instance;
}
private:
static Singleton *instance;
Singleton(){}
Singleton(const Singleton&) = delete;
void operator=(const Singleton&) = delete;
};
Singleton *Singleton::instance = nullptr;
单独将获取实例的接口拿出来分析,为什么不是线程安全的:
static Singleton*GetInstance(){
if(instance == nullptr){
instance = new Singleton();
}
return instance;
}
上述instance = new Singleton();
在宏观上看,整体可以划分为三个步骤:
- 调用malloc给对象分配内存;
- 调用对象的构造函数在内存上进行初始化工作;
- 将对象的地址赋值给instance.
假设这个过程,有两个线程执行,t1执行完1,2,时间片到了,被调度出去,此时t2来执行,一口气完成1,2,3,那么你觉得两个线程拿到的对象还是同一块内存上的对象吗? 当然不是。
从汇编指令上来讲步骤更多更复杂,所以我们需要借用锁来保证这个过程的正确性:
1. 最直接了当的加锁方式:不注重锁的粒度,影响性能
std::mutex mt;
class Singleton{
public:
static Singleton*GetInstance(){
std::lock_guard<std::mutex> guard(mt); //锁的粒度太大
if(instance == nullptr){
instance = new Singleton();
}
return instance;
}
private:
static Singleton *instance;
Singleton(){}
Singleton(const Singleton&) = delete;
void operator=(const Singleton&) = delete;
};
Singleton *Singleton::instance = nullptr;
假设该单例对象的构造函数会进行大量的初始化工作,我们将锁加在这里,粒度过大,会影响整体的性能,不妥。
2.注重锁的粒度,“锁+双重判断”:
std::mutex mt;
class Singleton{
public:
static Singleton*GetInstance(){
if(instance == nullptr){ //1重判断
std::lock_guard<std::mutex> guard(mt); //锁
if(instance == nullptr){ //2重判断
instance = new Singleton();
}
}
return instance;
}
private:
static Singleton *volatile instance;
Singleton(){}
Singleton(const Singleton&) = delete;
void operator=(const Singleton&) = delete;
};
Singleton *volatile Singleton:: instance = nullptr;
并且最好使用volatile 去修饰指针变量,保证线程不会缓存一份指针变量,引起误差。
总结:
double check + lock + volatile.