目录
面型对象设计八大原则
单例模式
定义:保证一个类仅有一个实例,并提供一个该实例的全局访问点
1. Motivation
在软件系统中,有些类必须保证他们在系统中只存在一个实例,以此来保证他们的逻辑正确性和效率
2. 实现
2.1 饿汉式
所谓饿汉式,即初始化即实例化,以后直接拿来用即可。(本身就是线程安全的)
class single{
private:
static single* p;
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::p = new single();
single* single::getinstance(){
return p;
}
但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序不确定的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例
2.2 懒汉式
懒汉式是指第一次使用的时候实例化,之后直接拿来用。但在第一次实例化的时候会有线程安全问题。
2.2.1 普通版 (即线程非安全版本):
class single{
private:
single(){};
~single(){}
static single* p;
public:
static single* getinstance();
static single* initance();
};
static single* single::p = nullptr;
singleton* singleton::initance(){
if (p == nullptr) {
p = new single();
}
return p;
}
2.2.2 线程安全版本 (锁的代价过高)
class single{
private:
single(){
pthread_mutex_init(&mutex);
};
~single(){}
static single* p;
public:
static pthread_mutex_t mutex;
static single* getinstance();
static single* initance();
};
static single* single::p = nullptr;
singleton* singleton::initance(){
pthread_mutex_lock(&mutex); // 加锁
if (p == nullptr){
p = new singleton();
}
pthread_mutex_unlock(&mutex);
return p;
}
2.2.3 双检查锁 (由于内存读写reorder不安全)
2.2.3.1最初版本:
class single{
private:
single(){
pthread_mutex_init(&mutex);
};
~single(){}
static single* p;
public:
static pthread_mutex_t mutex;
static single* getinstance();
static single* initance();
};
static single* single::p = nullptr;
singleton* singleton::initance(){
if (p == nullptr){ //锁前检查
pthread_mutex_lock(&mutex);
if (p == nullptr) // 锁后检查
p = new singleton();
pthread_mutex_unlock(&mutex);
}
return p;
}
此版本看似很完美,迷惑了很多人,但是由于内存读写reorder,会导致双检查锁失效
reorder:
代码有指令序列,指令序列并非总是按照我们想象的方式去进行(编译器优化),实际代码在指令层的时指令执行顺序有可能会和我们假设的不一样;
例如这句代码:p = new singleton(); 会被分为三步(默认顺序):
- 先分配内存
- 调用构造器对第一步分配的内存进行初始化
- 将内存地址给p
上面是我们假想的顺序,实际执行中有可能被reorder为:
- 先分配内存
- 将内存地址给p
- 调用构造器对分配的内存进行初始化
此时在第二步将内存给地址p的时候,p就不是null了;问题来了,此时另外一个或多个线程进来,进行 if (p == nullptr)的检查,此时p已经不是null了,直接返回。但此时返回的p不能使用。
2.2.3.2 改进版本:
- java 和 C# 使用volatile关键字解决,编译器对加了volatile关键字的变量,在赋值过程中是不会进行reorder的
- vc++自己也有个volatile实现版本,但是只有微软能用,并不是跨平台的
C++11版本之后的双检查锁实现:
2.2.3.2 优雅版本:
在《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象(函数内的static对象),这种方法不用加锁和解锁操作。当第一次访问getinstance()方法时才创建实例。
class single{
private:
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::getinstance(){
static single obj;
return &obj;
}
在C++ 0X 标准以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的(G++4.0及以上是支持的),C++0x之前仍需加锁。
3. 总结:
- 若是单线程,使用2.2.1 普通版已经足够了
- 若是多线程:
- 使用==2.2.2 线程安全版本 (锁的代价过高)==是不出错的,但是代价过于高
- 使用普通双检查锁要注意reorder带来的安全问题,直接使用C++11版本的跨平台实现
- 在C++ 0X 标准以后可以使用函数内的局部静态对象的线程安全来保证整体的线程安全
参考
- GeeKBand - 设计模式 李建忠
- 《Effective C++》