每一次实验课都要把上一节课的实验报告打印出来,交作业,这个时候的打印店总是很多人,这时候打印机就那么几台,粥少僧多的情况下,打印机怎么进行处理呢?
01 | 什么是单例模式?
-
概念
单例模式是属于创建型模式中的应用十分广泛的一种,它的设计目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享,简单来说就是在整个软件运行的生命周期里,单例模式保证一个类只能产生一个实例,确保该类的唯一实例性
-
疑问:为什么一定要保证类的唯一实例性呢?
主要为了解决一个全局使用的类被频繁的创建和销毁,浪费系统资源,防止多线程环境中资源使用异常导致系统信息错乱、崩溃的情况
比如,开篇中说到的打印机工作情况,如果打印机对接收到的文件不做任何处理,来啥打印啥,这样子的话就会导致A和B都在使用打印机打印实验报告,这时候打印机就疯狂的吐纸,吐出来的实验报告全是错乱的,上一页是A的封面,下一页是B的实验截图,场面十分的尴尬。
所以打印机实际上就是单例模式的一个应用场景,保证能最终控制打印机的类只有一个实例,谁先要用它,都必须在它空闲的时候才行,否则就要进行排队,保证正常运行的秩序。
-
分类
单例模式可以分为懒汉式、饿汉式,两者之间主要区别就是类实例初始化的时间不同
-
懒汉式:指系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。不叫它都懒得干活,所以称为懒汉式
-
优点:只有当使用的时候才进行初始化,节省内存资源
-
缺点:在多线程中式不安全的,需要考虑线程安全问题;存在内存泄漏问题
-
-
饿汉式:指系统一运行,就初始化创建实例,当需要时,直接调用即可。跟没吃过东西一样,一到小吃街就疯狂吃吃吃,压根不管贵不贵、好不好吃,所以称为饿汉式
-
优点:因为在系统一运行的时候就初始化了,所以没有线程安全问题
-
缺点:消耗的内存资源可能比懒汉式多(但是不会用的东西谁会卸写在系统里?既然写了那应该就式说未来会用到的,所以这点不知道算不算缺点),而且由于对象初始化在不同的编译环境中顺序式未定义的,这就有可能导致初始化还未完成的时候,其他调用的地方就开始使用这个未完全定义的实例
-
-
-
什么是线程安全?
前面讲单例模式分类的时候有一个常出现的词线程安全,那什么是线程安全呢?百度百科是这样解释的
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
-
怎么解决线程安全问题?
操作系统中关于进程/线程管理中有讲到互斥锁的概念,保证线程安全也是用到锁来确保资源的同一时刻的唯一操作权,简单来说就是给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用
-
特点
-
为了禁止外部构造和析构,禁止外部拷贝和赋值,确保实例的唯一性。所以将构造函数和析构函数以及拷贝构造和赋值构造函数设置为private类型
-
提供一个可以全局访问的用于获取唯一实例的静态函数
-
02 | 实现
因为对单例模式还是不太熟悉,所以下面是直接从网上扒的单例模式各类源码,再自己对着代码进行剖析,以此为基础进行单例模式的学习
懒汉式
前面提到过懒汉式单例模式有线程安全的问题,所以下面会分基础的懒汉式单例和解决线程安全的稍微没那么懒的懒汉式单例进行剖析
-
基础懒汉式单例
class Singleton { private: static Singleton* instance; private: Singleton() {}; ~Singleton() {}; Singleton(const Singleton&); Singleton& operator=(const Singleton&); public: static Singleton* getInstance() { if(instance == NULL) { instance = new Singleton(); } return instance; } }; Singleton* Singleton::instance = NULL;
从上面的基础源码中能看出来单例模式的特点:构造函数和析构函数以及拷贝构造和赋值构造函数设置为private类型;一个可以全局访问的用于获取唯一实例的静态函数
从基础源码上看,在单线程环境中,实例对象仅能通过
getInstance()
进行创建调用,if (instance == NULL)
保证了仅能创建一个实例对象。前面提到这个基础懒汉式在多线程环境中是存在线程安全问题的,也就是说它在多线程环境中,可能会创建出多个实例对象,尔后在销毁实例对象是会导致环境破坏以及一定的内存泄漏问题,理论上能大概想得到,但是还是实际上手检验一下才知道。下面参照源码写了个简单的测试Demo,试验以下多线程环境中基础懒汉式的运行是否存在线程安全问题#include <iostream> #include <pthread.h> using namespace std; #define THREADS_SIZE 5 class Singleton_BasicLazy { private: static Singleton_BasicLazy* m_Instance; private: Singleton_BasicLazy(){cout << "基础懒汉出街" << endl;}; ~Singleton_BasicLazy(){}; Singleton_BasicLazy(const Singleton_BasicLazy& i_singleton); const Singleton_BasicLazy &operator=(const Singleton_BasicLazy& i_singleton); public: static Singleton_BasicLazy* getInstance() { if (nullptr == m_Instance) { m_Instance = new Singleton_BasicLazy; } return m_Instance; } }; Singleton_BasicLazy* Singleton_BasicLazy::m_Instance = nullptr; void* callback_Hello(void* i_threadid) { // 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收 pthread_detach(pthread_self()); // 将传入的参数由无类型指针强制转换成整形数指针 cout << "线程 ID:[" << *((int *)i_threadid) << "]" << endl; Singleton_BasicLazy::getInstance(); pthread_exit(NULL); } int main() { pthread_t threads[THREADS_SIZE] = {0}; int ret = 0; int index[THREADS_SIZE] = {0}; cout << "==========> Ready Go <==========" << endl; for (int i = 0; i < THREADS_SIZE; i++) { index[i] = i; ret = pthread_create(&threads[i], NULL, callback_Hello, (void*)&(index[i])); if (ret) { cout << "创建线程失败" << endl; exit(-1); } } return 0; }
从测试结果上可以看到打印了两次基础懒汉出街,也就说明在多线程环境中,基础懒汉式单例模式确实存在线程安全问题,打破了唯一实例的规则,所以基础懒汉式单例仅适用于单线程环境。
-
加锁懒汉式单例
为了让懒汉稍微靠谱一点,让他有点激情,人们选择了给他上把锁……
class SingleInstance { public: static SingleInstance *GetInstance() { if (m_SingleInstance == nullptr) { unique_lock<mutex> lock(m_Mutex); if (m_SingleInstance == nullptr) { auto temp = new SingleInstance; m_SingleInstance = temp; } } return m_SingleInstance; } private: SingleInstance(); ~SingleInstance(); SingleInstance(const SingleInstance &signal); const SingleInstance &operator=(const SingleInstance &signal); private: static SingleInstance *m_SingleInstance; static std::mutex m_Mutex; }; SingleInstance *SingleInstance::m_SingleInstance = nullptr; std::mutex SingleInstance::m_Mutex;
与基础懒汉式相比,加锁的懒汉改变之处就是在
GetInstance()
中使用了双检锁技术,保证每一次仅有一个线程能进入第二个if (m_SingleInstance == nullptr)
,为什么不在外面加一把锁,而是在第一个if
之后加呢?通常线程安全问题都是发生在创建实例对象这一步骤中,那么只需要保证m_SingleInstance == nullptr
之前是安全的即可,同时加锁堆系统资源来说也是一笔不小的开销,如果每一次调用GetInstance()
都加一次锁的话,会浪费很多的系统资源同样的,参考以上源码,自己编写了一个测试Demo进行检验,Demo代码如下::
#include <iostream> #include <mutex> #include <pthread.h> using namespace std; #define THREADS_SIZE 5 class Singleton_MutexLazy { private: static Singleton_MutexLazy* m_Instance; static mutex m_Mutex; private: Singleton_MutexLazy(){}; ~Singleton_MutexLazy(){}; Singleton_MutexLazy(const Singleton_MutexLazy& i_singleton); const Singleton_MutexLazy &operator=(const Singleton_MutexLazy& i_singleton); public: static Singleton_MutexLazy* getInstance() { if (nullptr == m_Instance) { unique_lock<mutex> lock(m_Mutex); if (nullptr == m_Instance) { m_Instance = new Singleton_MutexLazy; } } return m_Instance; } }; Singleton_MutexLazy* Singleton_MutexLazy::m_Instance = NULL; mutex Singleton_MutexLazy::m_Mutex; void* callback_Hello(void* i_threadid) { // 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收 pthread_detach(pthread_self()); // 将传入的参数由无类型指针强制转换成整形数指针 cout << "线程 ID:[" << *((int *)i_threadid) << "]" << endl; Singleton_MutexLazy::getInstance(); pthread_exit(NULL); } int main() { pthread_t threads[THREADS_SIZE] = {0}; int ret = 0; int index[THREADS_SIZE] = {0}; cout << "==========> Ready Go <==========" << endl; for (int i = 0; i < THREADS_SIZE; i++) { index[i] = i; ret = pthread_create(&threads[i], NULL, callback_Hello, (void*)&(index[i])); if (ret) { cout << "创建线程失败" << endl; exit(-1); } } return 0; }
从结果上可以看到,这一次只存在一次加锁懒汉出街,说明加锁版懒汉单例在多线程环境中是安全的,但是加锁意味着系统 的开销增加。
-
内部静态变量式懒汉
C++11中规定了内部静态变量在多线程环境中初始化的行为,要求编译器保证内部静态变量的线程安全性,所以在C++11标准下,出现了一种“优雅且安全“的懒汉实现方法 —— 使用函数内的静态变量对象,这样只有当第一次访问
GetInstance()
时才创建实例class Single { public: static Single GetInstance() { static Single signal; return signal; } private: Single(); ~Single(); Single(const Single &signal); const Single &operator=(const Single &signal); };
从源码上就能看出来,相比之前的源码干净整洁的多,减少了private中的
instance
, 也不需要加锁,为什么这样子就能做到线程安全呢?主要是GetInstance()
中的Static Single signal
这一行,因为静态局部变量只在当前函数内有效,其他函数是无法访问的,而且只在第一次被调用的时候初始化,也存储在静态存储区,生命周期从第一次被初始化起直到程序结束
老样子,Demo双手奉上#include <iostream> #include <pthread.h> using namespace std; #define THREADS_SIZE 5 class Singleton_StaicInLazy { private: Singleton_StaicInLazy(){cout << "优雅懒汉出街" << endl;}; ~Singleton_StaicInLazy(){}; Singleton_StaicInLazy(const Singleton_StaicInLazy& i_singleton); const Singleton_StaicInLazy &operator=(const Singleton_StaicInLazy& i_singleton); public: static Singleton_StaicInLazy& getInstance() { static Singleton_StaicInLazy m_Instance; return m_Instance; } }; void* callback_Hello(void* i_threadid) { // 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收 pthread_detach(pthread_self()); // 将传入的参数由无类型指针强制转换成整形数指针 cout << "线程 ID:[" << *((int *)i_threadid) << "]" << endl; Singleton_StaicInLazy::getInstance(); pthread_exit(NULL); } int main() { pthread_t threads[THREADS_SIZE] = {0}; int ret = 0; int index[THREADS_SIZE] = {0}; cout << "==========> Ready Go <==========" << endl; for (int i = 0; i < THREADS_SIZE; i++) { index[i] = i; ret = pthread_create(&threads[i], NULL, callback_Hello, (void*)&(index[i])); if (ret) { cout << "创建线程失败" << endl; exit(-1); } } return 0; }
结果和加锁懒汉式单例一样,只进行了一次构造函数的调用,说明其在多线程环境中也是安全的
饿汉式
饿汉式单例模式:指单例实例在程序运行时就被立即执行初始化的单例模式。
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton();
~Singleton();
Singleton(const Singleton &signal);
const Singleton &operator=(const Singleton &signal);
private:
static Singleton *g_pSingleton;
};
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton();
Singleton* Singleton::GetInstance()
{
return g_pSingleton;
}
相对懒汉式来说,由于在main()
函数之间就完成了初始化的动作,所以在多线程环境中式没有线程安全问题的。还是原来的配方,Demo测试走起
#include <iostream>
#include <pthread.h>
using namespace std;
#define THREADS_SIZE 5
class Singleton_Hungry
{
private:
static Singleton_Hungry* m_Instance;
private:
Singleton_Hungry(){cout << "饿汉出街" << endl;};
~Singleton_Hungry(){};
Singleton_Hungry(const Singleton_Hungry& i_singleton);
const Singleton_Hungry &operator=(const Singleton_Hungry& i_singleton);
public:
static Singleton_Hungry* getInstance()
{
if (nullptr == m_Instance)
{
m_Instance = new Singleton_Hungry;
}
return m_Instance;
}
};
Singleton_Hungry* Singleton_Hungry::m_Instance = new Singleton_Hungry;
void* callback_Hello(void* i_threadid)
{
// 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收
pthread_detach(pthread_self());
// 将传入的参数由无类型指针强制转换成整形数指针
cout << "线程 ID:[" << *((int *)i_threadid) << "]" << endl;
Singleton_Hungry::getInstance();
pthread_exit(NULL);
}
int main()
{
pthread_t threads[THREADS_SIZE] = {0};
int ret = 0;
int index[THREADS_SIZE] = {0};
cout << "==========> Ready Go <==========" << endl;
for (int i = 0; i < THREADS_SIZE; i++)
{
index[i] = i;
ret = pthread_create(&threads[i], NULL, callback_Hello, (void*)&(index[i]));
if (ret)
{
cout << "创建线程失败" << endl;
exit(-1);
}
}
return 0;
}
03 | 总结
单例模式的精髓就在于保证一个类仅有一个实例,并提供一个访问它的全局访问点,无论是懒汉式还是饿汉式,都需要遵循这一点,两者之间的主要区别就是
-
懒汉式主要通过时间资源替换空间资源,比较适合在访问量比较小的软件系统中使用(从上面的分析可以看到,使用内部静态变量单例模式比较简洁明了且具有一定的线程安全性)
-
饿汉式主要通过空间资源替换时间资源,比较适合在访问量比较大,或者说线程比较多的软件系统中使用