模式定义
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
模式动机
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。
模式结构
单例模式包含如下角色:
- Singleton:单例
C++单例的实现
基础要点
- 意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 主要解决: 一个全局使用的类频繁地创建与销毁。
- 何时使用: 当您想控制实例数目,节省系统资源的时候。
- 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 关键代码: 构造函数是私有的。
懒汉式
//懒汉式的方法是直到使用时才实例化对象,也就说直到调用getInstance() 方法的时候才 new 一个单例的对象。好处是如果被调用就不会占用内存。
class Singleton_lazy {
private:
Singleton_lazy() {
cout << "Singleton_lazy() 构造函数被调用!" << endl;
}
public:
static Singleton_lazy* getInstance() { //公共接口
if (pSingleton == nullptr) {
pSingleton = new Singleton_lazy;
}
return pSingleton;
}
private:
static Singleton_lazy* pSingleton; //静态成员变量所有类共享一份
};
Singleton_lazy* Singleton_lazy::pSingleton = nullptr;
void test1() {
Singleton_lazy* s1 = Singleton_lazy::getInstance();
Singleton_lazy* s2 = Singleton_lazy::getInstance();
}
上述代码输出为
Singleton_lazy() 构造函数被调用!
由此可见,两次获取一个对象,只调用了一次构造函数,最基本的单例模式实现了。但是它是有哪些问题呢?
-
线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在
if
中判断pSingleton
是空的,于是开始实例化单例;同时第 2 个线程也尝试获取单例,这个时候判断pSingleton
还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁 -
内存泄漏. 注意到类中只负责
new
出对象,却没有负责delete
对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法: 使用共享指针,嵌套垃圾回收;
关于线程安全问题,加一把锁就能解决,此处不做过的多赘述,详情请见 C++ 11 互斥量简介。
其实就单例模式而言,回收内存意义不大,因为在所有程序中该对象只有一份,但是聊胜于无,既然想要回收那就不能显式调用 delete
去回收,因为有可能其他人在使用的时候不小心进行了释放,导致产生莫名的 BUG
,所以设计嵌套类的目的是为了定义它的静态子对象,在程序结束时会调用该子对象的析构函数以释放唯一的实例。
使用嵌套类 + 锁实现的单例模式
class Singleton_lazy {
private:
Singleton_lazy() {
cout << "Singleton_lazy() 构造函数被调用!" << endl;
}
class Garbo { //嵌套类,它的唯一工作就是在析构函数中释放实例
public:
~Garbo() {
if (Singleton_lazy::pSingleton != nullptr) { //可以在这里销毁所有的资源
delete Singleton_lazy::pSingleton;
cout << "Singleton_lazy() 被释放!" << endl;
Singleton_lazy::pSingleton = nullptr;
}
}
};
public:
static Singleton_lazy* getInstance() {
//在判断前,使用锁来保证线程安全,该锁自动加锁,自动解锁
if(pSingleton == nullptr){ //双检锁
lock_guard<mutex> lk(m_mutex);
if (pSingleton == nullptr) {
pSingleton = new Singleton_lazy;
}
}
return pSingleton;
}
private:
static Singleton_lazy* pSingleton;
static Garbo garbo;//定义一个子对象,在程序结束时会调用它的析构函数
static mutex m_mutex;//定义一把锁,用来保证线程安全
};
Singleton_lazy* Singleton_lazy::pSingleton = nullptr;
Singleton_lazy::Garbo Singleton_lazy::garbo;
mutex Singleton_lazy::m_mutex;
void test1() {
Singleton_lazy* s1 = Singleton_lazy::getInstance();
Singleton_lazy* s2 = Singleton_lazy::getInstance();
}
使用智能指针 + 锁实现的单例模式
也可以使用C++ 11 中的智能指针 share_ptr
进行解决,当智能指针在被释放会自动释放自己占据的内存,请见智能指针。
class Singleton_lazy {
private:
Singleton_lazy() {
cout << "Singleton_lazy() 构造函数被调用!" << endl;
}
public:
using Ptr = shared_ptr<Singleton_lazy>; //使用智能指针
static Ptr getInstance() { //第一处更改,返回智能指针
if(pSingleton == nullptr){ //双检锁,避免重复加锁
lock_guard<mutex> lk(m_mutex);
if (pSingleton == nullptr) {
pSingleton = shared_ptr<Singleton_lazy>(new Singleton_lazy); //第二处更改,创建智能指针对象
}
}
return pSingleton;
}
private:
static Ptr pSingleton; //第三处更改
static mutex m_mutex;
};
Singleton_lazy::Ptr Singleton_lazy::pSingleton = nullptr; //第四处
mutex Singleton_lazy::m_mutex;
void test1() {
Singleton_lazy::Ptr s1 = Singleton_lazy::getInstance(); //第五处
Singleton_lazy::Ptr s2 = Singleton_lazy::getInstance();
}
shared_ptr
和 mutex
都是 C++ 11的标准,以上这种方法的优点是:
- 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏
- 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。
因此这里还有第三种的基于 Magic Staic的方法达到线程安全
基于局部静态变量的单例模式(magic static )
class Singleton_lazy {
private:
Singleton_lazy() {
cout << "Singleton_lazy() 构造函数被调用!" << endl;
}
public:
static Singleton_lazy& getInstance() {
static Singleton_lazy pSingleton;
return pSingleton;
}
~Singleton_lazy() {
cout << "Singleton_lazy() 析构函数被调用!" << endl;
}
private:
};
void test1() {
Singleton_lazy& s1 = Singleton_lazy::getInstance();
Singleton_lazy& s2 = Singleton_lazy::getInstance();
}
上述代码输出为
Singleton_lazy() 构造函数被调用!
Singleton_lazy() 析构函数被调用!
这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
- 不需要使用共享指针,代码简洁,不需要担心内存释放的问题;
- 注意在使用的时候需要声明单例的引用
Singleton_lazy&
才能获取对象。
饿汉式
饿汉式是一个线程安全的写法,但是由于先初始化,所以会产生垃圾对象。
优点:没有加锁这一步,执行效率提高
缺点:在代码执行前就加载好了,浪费内存
比较简单也没什么好说的。。。
class Singleton_hungry {
private:
Singleton_hungry() {}
static Singleton_hungry* getInstance() { return pSingleton; }
private:
static Singleton_hungry* pSingleton;
};
Singleton_hungry* Singleton_hungry::pSingleton = new Singleton_hungry;