单例模式学习笔记-两种经典实现及单/多线程环境的验证
什么是单例模式?
定义
- 确保一个类只有一个实例(也就是类的对象)
- 并且提供一个全局的访问点(外部通过这个访问点来访问该类的唯一实例)。
功能
使得类的一个对象成为系统中的唯一实例。举个例子:一个操作系统中可以存在多个打印任务,但我们只有一个打印机,同一时间只能有一个正在工作的任务,这个时候内存中打印机的这个类就必须是单例的,否则的话就可以同时执行多个打印任务,这显然会造成错误。
两种经典单例–饿汉式&&懒汉式
单例模式的两种经典实现与 “立即加载” 和 “延迟加载” 两个概念相关。
- “立即加载”:在类开始初始化的时候就主动创建实例
- “延迟加载”:等到真正用到的时候再去创建实例,不用时不主动创建实例
在单线程中,单例模式根据实例化对象的时机不同,分为饿汉式(立即加载)和懒汉式(延迟加载)
- “饿汉式单例”:程序加载时,就已经创建实例
- “懒汉式单例”:用到时,实例才创建
单线程下的两种单例方式实现
单线程-饿汉式
#include <iostream>
using namespace std;
class Singleton_Hungry
{
private:
Singleton_Hungry()
{
cout << "我是饿汉式,在程序加载时,我就已经存在了。" << endl;
}
static Singleton_Hungry* singleton;
public:
static Singleton_Hungry* getInstace()
{
return singleton;
}
};
//静态属性类外初始化
Singleton_Hungry* Singleton_Hungry::singleton = new Singleton_Hungry;
int main(int argc, char *argv[])
{
Singleton_Hungry *hungry1 = Singleton_Hungry::getInstace();
Singleton_Hungry *hungry2 = Singleton_Hungry::getInstace();
cout << "hungry1地址:" << hungry1 << "\nhungry2地址:" << hungry2 << endl;
return 0;
}
程序执行结果:
上述单例模式被加载时就构建了一个实例,供系统使用。而且这个类在整个生命周期只会加载一次,因此只会构建一个实例,充分保证单例。
单线程-懒汉式
#include <iostream>
using namespace std;
class Singleton_Lazy
{
private:
Singleton_Lazy()
{
cout << "我是懒汉式,在别人需要我的时候,我才现身。" << endl;
}
static Singleton_Lazy* singleton;
public:
static Singleton_Lazy* getInstance()
{
if (NULL == singleton)
{
singleton = new Singleton_Lazy;
}
return singleton;
}
};
//静态属性类外初始化
Singleton_Lazy* Singleton_Lazy::singleton = NULL;
int main(int argc, char *argv[])
{
Singleton_Lazy *lazy1 = Singleton_Lazy::getInstance();
Singleton_Lazy *lazy2 = Singleton_Lazy::getInstance();
cout << "lazy1地址:" << lazy1 << "\nlazy2地址:" << lazy1 << endl;
return 0;
}
程序执行结果:
从懒汉式单例可以看出,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象供自己使用。
多线程模式下两种单例模式的实现
在单线程环境下,无论是饿汉式单例还是懒汉式单例,都能正常工作。 但是在多线程环境下时候单例模式时,应特别注意 “线程安全” 问题。
在多线程环境中
- 饿汉式单例天生就是线程安全的,可直接用于多线程而不会出任何问题;
- 懒汉式单例本身是非线程安全的,一旦出现多个实例的情况,会背离单例模式设计初衷。
下面就来说明以下三个问题:
- 为什么说饿汉式单例天生就是线程安全的?
- 传统的懒汉式单例为什么是非线程安全的?
- 怎么修改传统的懒汉式单例,使其线程变得安全?
为了能够更好的观察到单例模式的实现是否是线程安全的,我们提供了一个简单的测试程序来验证。该示例程序的判断原理是:
- 开启多个线程来分别获取单例,然后打印它们所获取到的单例的地址。若它们获取的单例是相同的(该单例模式的实现是线程安全的),那么它们的地址值一定完全一致;
- 若它们的地址值不完全一致,那么获取的单例必定不是同一个,即该单例模式的实现不是线程安全的,是多例的。
多线程-饿汉式(线程安全)
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex mu;//线程互斥对象
class Singleton_Hungry
{
private:
Singleton_Hungry()
{
cout << "======我是饿汉式,在程序加载时,我就已经存在了。======" << endl;
}
static Singleton_Hungry* singleton;
public:
static Singleton_Hungry* getInstance()
{
return singleton;
}
};
//静态属性类外初始化
Singleton_Hungry* Singleton_Hungry::singleton = new Singleton_Hungry;
void thread01()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread01 working....】" << endl;
Singleton_Hungry *Hungry1 = Singleton_Hungry::getInstance();
cout << "thread01创建单例Hungry1地址:" << Hungry1 <<"\n"<< endl;
}
}
void thread02()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread02 working....】" << endl;
Singleton_Hungry *Hungry2 = Singleton_Hungry::getInstance();
cout << "thread02创建单例Hungry2地址:" << Hungry2 <<"\n"<< endl;
}
}
int main(int argc, char *argv[])
{
thread thread1(thread01);
thread thread2(thread02);
thread1.detach();
thread2.detach();
for (int i = 0; i < 3; i++)
{
cout << "Main thread working..." << endl;
Singleton_Hungry *main = Singleton_Hungry::getInstance();
cout << "Main 创建单例Hungry地址:" << main << endl;
}
return 0;
}
从执行结果来看,上述“饿汉式”单例类被加载时,会实例化一个对象用来使用。由于“饿汉式”单例模式具有:
- 在线程访问单例对象之前就已经创建好了
- 一个类在生命周期内仅会被加载一次
因此该“饿汉式”单例类只会创建一个实例,即线程每次也只会拿到这个唯一对象,因此就说,饿汉式单例天生就是线程安全的。但 “饿汉式”在类加载时就初始化,会浪费内存。
多线程-懒汉式
“懒汉式单例”线程不安全举例
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex mu;//线程互斥对象
class Singleton_Lazy
{
private:
Singleton_Lazy()
{
cout << "我是懒汉式,在别人需要我的时候,我才现身。" << endl;
}
static Singleton_Lazy* singleton;
public:
static Singleton_Lazy* getInstance()
{
if (NULL == singleton) /**问题所在!!!!**/
{
singleton = new Singleton_Lazy;
}
return singleton;
}
};
Singleton_Lazy* Singleton_Lazy::singleton = NULL;
void thread01()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread01 working....】" << endl;
Singleton_Lazy *Lazy1 = Singleton_Lazy::getInstance();
cout << "thread01创建单例Lazy1地址:" << Lazy1 <<"\n"<< endl;
}
}
void thread02()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread02 working....】" << endl;
Singleton_Lazy *Lazy2 = Singleton_Lazy::getInstance();
cout << "thread02创建单例Lazy2地址:" << Lazy2 <<"\n"<< endl;
}
}
int main(int argc, char *argv[])
{
thread thread1(thread01);
thread thread2(thread02);
thread1.detach();
thread2.detach();
for (int i = 0; i < 3; i++)
{
cout << "Main thread working..." << endl;
Singleton_Lazy *main = Singleton_Lazy::getInstance();
cout << "Main 创建单例Lazy地址:" << main << endl;
}
return 0;
}
上面发生的非线程安全的原因是:会同时有多个线程进入 if (NULL == singleton) 这个代码块,当这种情况发生时,发生了内存泄漏,创建出多个实例,违背了单例模式的初衷,所以说“懒汉式单例是非线程安全的”。
“懒汉式单例”多线程–“双重检测”加锁
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex mu;//线程互斥对象
class Singleton_Lazy
{
private:
Singleton_Lazy()
{
cout << "我是懒汉式,在别人需要我的时候,我才现身。" << endl;
}
static Singleton_Lazy* singleton;
public:
static Singleton_Lazy* getInstance()
{
//双重检测加锁
if (NULL == singleton)
{
mu.lock();
if (NULL == singleton)
{
singleton = new Singleton_Lazy;
}
mu.unlock();
}
return singleton;
}
};
Singleton_Lazy* Singleton_Lazy::singleton = NULL;
void thread01()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread01 working....】" << endl;
Singleton_Lazy *Lazy1 = Singleton_Lazy::getInstance();
cout << "thread01创建单例Lazy1地址:" << Lazy1 <<"\n"<< endl;
}
}
void thread02()
{
for (int i = 0; i < 3; i++)
{
cout << "【thread02 working....】" << endl;
Singleton_Lazy *Lazy2 = Singleton_Lazy::getInstance();
cout << "thread02创建单例Lazy2地址:" << Lazy2 <<"\n"<< endl;
}
}
int main(int argc, char *argv[])
{
thread thread1(thread01);
thread thread2(thread02);
thread1.detach();
thread2.detach();
for (int i = 0; i < 3; i++)
{
cout << "Main thread working..." << endl;
Singleton_Lazy *main = Singleton_Lazy::getInstance();
cout << "Main 创建单例Lazy地址:" << main << endl;
}
return 0;
}
如上述代码所示,为了保证单例的前提下提升运行效率,需要对if (NULL == singleton)做二次检查,目的是避开过多的同步 (因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)
以上是我结合下面的参考文章经过实践后得出的关于单例模式学习的整理笔记。