应用实例:线程池,日志类,windows系统的任务管理器
一个单例模式应具备以下特征:
- 只能实例化同一个对象
- 全局访问
- 禁止拷贝
- 线程安全
下面一步步去构思一个单例模式。
针对第一条,可以将构造函数权限设为private,如果是public那么每次实例化调用构造函数,对象的内存地址都不同,也就是说只准调用一次构造函数。那问题来了,构造函数都private了,怎么实例化对象?显然要用某个public方法来调用,这又有问题:都没实例化对象,怎么调用public方法?容易想到static public方法
针对第二条,全局性很容易想到静态函数,它是属于类的,而不是属于某个对象的。
第三条很容易,将拷贝构造函数和复制运算符声明为private即可。
综上,单例类的雏形应该是这样的:1
2
3
4
5
6
7
8
9
10
11
12class Singleton
{
public:
// 单例方法
private: //构造函数或析构函数为私有函数,所以该类是无法被继承的,
Singleton(){std::cout<<"单例构造"<<endl; }
~Singleton(){std::cout<<"单例析构"<<endl; }
Singleton(const Singleton &);
Singleton& operator=(const Singleton&);
static Singleton* m;
};
关键就是怎么实现public static方法。
测试1
首先想到这样的方法:1
2
3
4
5static Singleton instance()
{
Singleton s;
return s;
}
结果报错构造函数和析构函数是private
,第一行就编译不过
测试2
测试下面这种方法1
2
3
4
5
6
7
8
9static Singleton* instance1()
{
Singleton *s = new Singleton();
return s;
}
Singleton* s1 = Singleton::instance1();
Singleton *s2 = Singleton::instance1();
delete s1;
delete s2;
结果发现有两个构造函数,而且无法析构。
有缺陷的懒汉模式
1 | class Singleton |
测试:1
2
3
4
5
6
7// 在类外要初始化静态变量: SingleTon* SingleTon::instance = nullptr;
Singleton* s = Singleton::LazyInstance();
std::cout<<s<<endl;
Singleton* s2 = s;
std::cout<<s2<<endl;
delete s;
// delete s2; 不能这样,否则二次析构
s和s2的内存地址相同,说明是单例。但类中只负责new出对象,却没有负责delete对象,结果发现只调用一次构造函数,还需要手动delete s
。容易想到用智能指针解决
如果有两个线程,假设pthread_1刚判断完 intance 为NULL 为真,准备创建实例的时候,切换到了pthread_2, 此时pthread_2也判断intance为NULL为真,创建了一个实例,再切回pthread_1的时候继续创建一个实例返回,那么此时就不再满足单例模式的要求,显然要加锁
双检锁+智能指针的懒汉模式
1 | class SingleTon |
1 | // 在类外要初始化静态变量: SingleTon::Ptr SingleTon::instance = nullptr; |
结果二者地址相同,也运行了析构函数。加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处:只有判断指针为空的时候才加锁,避免每次调用getInstance
的方法都加锁
缺点:使用智能指针会要求用户也得使用智能指针;使用锁也有开销; 在某些平台,双检锁会失效
饿汉模式
优点:不需要加锁,执行效率高,线程安全的
缺点:初始化即实例化,浪费内存1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Singelton
{
private:
Singelton(){}
static Singelton *single;
public:
static Singelton *GetSingelton();
};
// 饿汉模式的关键:初始化即实例化
Singelton *Singelton::single = new Singelton;
Singelton *Singelton::GetSingelton()
{
// 不再需要进行实例化
//if(single == nullptr){
// single = new Singelton;
//}
return single;
}
最推荐:Meyers模式
1 | class Singleton |
这种方式所用到的特性是在C++11标准中的Magic Static特性。如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。
在 MeyersInstance() 函数内定义局部静态变量的好处是,构造函数只会在第一次调用MeyersInstance() 时被初始化, 保证了成员变量和 Singleton 本身的初始化顺序。
它还有一个潜在的安全措施, MeyersInstance() 返回的是对局部静态变量的引用, 如果返回的是指针, MeyersInstance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁。