Singleton(单例)模式
特点
- 保证一个类只创建一个实例。
- 提供对该实例的全局访问点。
应用
- 日志类,一个应用往往只对应一个日志实例。
- 配置类,应用的配置集中管理,并提供全局访问。
- 管理器,比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例。
- 共享资源类,加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。
原则
- 构造私有。
- 以静态方法或者枚举返回实例。
- 确保实例只有一个,尤其是多线程环境。
- 确保反序列化时不会重新构建对象。
常用解法
Lazy Singleton,Eager Singleton,Meyers Singleton,Double-Checked Locking Pattern,静态内部类实现单例,etc.
1 Lazy Singleton:只适用于单线程环境
//头文件中
class Singleton {
public:
static Singleton& Instance() {
if (instance_ == NULL) instance_ = new Singleton();
return *instance_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* instance_; //static成员需在类外初始化
};
// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆
实现中构造函数被声明为私有方法,这样从根本上杜绝外部使用构造函数生成新的实例,同时禁用拷贝函数与赋值操作符(声明为私有但是不提供实现)避免通过拷贝函数或赋值操作生成新实例。
提供静态方法Instance() 作为实例全局访问点,该方法中先判断有没有现成的实例,如果有直接返回,如果没有则生成新实例并把实例的指针保存到私有的静态属性中。
注意,这里Instance() 返回的是实例的引用而不是指针,如果返回的是指针可能会有被外部调用者delete 掉的隐患,所以这里返回引用会更加保险一些。并且直到Instance() 被访问,才会生成实例,这种特性被称为延迟初始化(Lazy initialization),这在一些初始化时消耗较大的情况有很大优势。
Lazy Singleton不是线程安全的,比如现在有线程A和线程B,都通过instance_ == NULL 的判断,那么线程A和B都会创建新实例。单例模式保证生成唯一实例的规则被打破了。
2 Eager Singleton
//头文件中
class Singleton {
public:
static Singleton& Instance() {
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton instance;
}
// Singleton实例初始化,前面也可以定义为指针(此时需要new Singleton())
Singleton Singleton::instance; // 前面不能加static,会和类外全局static混淆
这种实现在程序开始(静态属性instance初始化)时就完成了实例的创建。这正好和上述的Lazy Singleton相反。
由于在main函数之前初始化,所以没有线程安全的问题,但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用 Instance() 方法会返回一个未定义的实例。假设调用该静态函数可以实现单例模式,仍然会因为过早地创建实例,从而降低内存的使用效率。
3 Meyers Singleton
它其实是使用了C++中成员函数的静态变量的特点:静态局部变量在第一次使用时初始化,并不会销毁直到程序退出。
class Singleton {
public:
static Singleton& Instance() {
static Singleton instance;
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
Scott Meyers使用local static对象(函数内的static对象)。当第一次访问Instance() 方法时才创建实例。C++0x之后是该实现线程安全的,编译器支持程度不一定,G++4.0及以上是支持的。
4 懒汉模式加锁:支持多线程,但效率不高
//头文件中
class Singleton {
public:
static Singleton& Instance() {
Lock lock;
if (instance_ == NULL) instance_ = new Singleton();
return *instance_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* instance_; //static成员需在类外初始化
};
// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆
5 Double-Checked Locking Pattern
Lazy Singleton的一种线程安全改造是在Instance() 中每次判断是否为NULL前加锁,但是加锁是很慢的。 而实际上只有第一次实例创建的时候才需要加锁。双检测锁模式被提出来,改造之后大致是这样
class Singleton {
public:
static Singleton& Instance() {
if (instance_ == NULL) {
Lock lock; //基于作用域的加锁,超出作用域,自动调用析构函数解锁
if (instance_ == NULL) instance_ = new Singleton();
}
return *instance_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* instance_; //static成员需在类外初始化
};
// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆
既然只需要在第一次初始化的时候加锁,那么在这之前判断一下实例有没有被创建就可以了,所以多在加锁之前多加一层判断,需要判断两次所有叫Double-Checked。理论上问题解决了,但是在实践中有很多坑,如指令重排、多核处理器等问题让DCLP实现起来比较复杂比如需要使用内存屏障。
6 静态内部类实现单例:按需创建
class Singleton {
public:
static Singleton& Instance() {
return *Nested->instance_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
class Nested {
private:
static Nested();
~Nested();
static Singleton* instance_ = new Singleton();
};
static Nested Nested;
};
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance_,故而不占内存。
|