基本概念
单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。在代码上表现为整个程序中只调用一次该类的构造函数。
GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
使用场景
在应用系统开发中,我们常常有以下需求:
在多个线程之间,比如初始化一次socket资源;比如servlet环境,共享同一个资源或者操作同一个对象
在整个程序空间使用全局变量,共享资源
大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
因为Singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,Singleton模式就派上用场了。
实现步骤
构造函数私有化
提供一个全局的静态方法(全局访问点)
在类中定义一个静态指针,指向本类的变量
懒汉式单例模式
#include<iostream>
using namespace std;
class Singleton
{
private:
static Singleton * m_psl;
Singleton()
{
cout<<"constructor"<<endl;
}
// ~Singleton()
// {
// if(m_psl != NULL)
// {
// delete m_psl;
// m_psl = NULL;
// }
// }
public:
static void freeInstance()
{
if(m_psl != NULL)
{
delete m_psl;
m_psl = NULL;
}
}
static Singleton *getInstance()
{
if(m_psl == NULL)//整个程序中确实没有其他人创建过该类的实例才创建
{
m_psl = new Singleton;//等到需要创建对象的时候才创建对象--很懒
}
return m_psl;
}
};
Singleton * Singleton::m_psl = NULL;//并没有直接创建对象
int main()
{
Singleton * p1 = Singleton::getInstance();
Singleton * p2 = Singleton::getInstance();
if(p1 == p2)
{
cout<<"你好they are same"<<endl;
}else{
cout<<"你好not same"<<endl;
}
Singleton::freeInstance();
return 0;
}
饿汉式单例模式
#include<iostream>
using namespace std;
class Singleton
{
private:
static Singleton * m_psl;//私有全局静态成员变量--类的对象指针
Singleton()//私有构造函数
{
cout<<"constructor"<<endl;
}
public:
static void freeInstance()
{
if(m_psl != NULL)
{
delete m_psl;
m_psl = NULL;
}
}
static Singleton *getInstance()//全局静态成员函数--访问该类的实例
{
return m_psl;//直接返回该类实例的指针
}
};
Singleton * Singleton::m_psl = new Singleton;//不管你创建不创建实例,均把实例new出来
int main()
{
Singleton * p1 = Singleton::getInstance();
Singleton * p2 = Singleton::getInstance();
if(p1 == p2)
{
cout<<"they are same"<<endl;
}else{
cout<<"not same"<<endl;
}
Singleton::freeInstance();
return 0;
}
多线程下的单例模式
“懒汉”模式虽然有优点,但是每次调用GetInstance()静态方法时,必须判断NULL == m_instance,使程序相对开销增大。
多线程中会导致多个实例的产生,从而导致运行代码不正确以及内存的泄露。不符合单例模式的定义!
需要提供释放资源的函数
讨论: 这是因为C++中构造函数并不是线程安全的。
C++中的构造函数简单来说分两步:
第一步:内存分配
第二步:初始化成员变量
由于多线程的关系,可能当我们在分配内存好了以后,还没来得急初始化成员变量,就进行线程切换,另外一个线程拿到所有权后,由于内存已经分配了,但是变量初始化还没进行,因此打印成员变量的相关值会发生不一致现象。
懒汉式单例模式的问题抛出–破坏了单例模式的定义
#include<iostream>
#include <thread>
#include <unistd.h>
#include <thread>
using namespace std;
/*单例模式的类--这个类的实例只有一个--整个程序中该类的构造函数只应该被调用一次*/
class Singleton
{
private:
static Singleton * m_psl;//2. 定义一个静态指针,指向本类的变量
static int cnt;//静态变量,记录构造函数被调用的次数
/*1. 构造函数私有化*/
Singleton()
{
cout<<"constructor begin\n"<<endl;
sleep(2);//睡眠2秒钟--睡眠过程中会有其他线程进入构造函数
cout<<"constructor end\n"<<endl;
}
public:
/*全局静态函数--释放内存*/
static void freeInstance()
{
if(m_psl != NULL)
{
delete m_psl;
m_psl = NULL;
}
}
/*3. 提供一个全局的静态方法--类的全局访问点*/
static Singleton *getInstance()
{
if(m_psl == NULL)//进行判断是否有线程已经创建类的实例
{
cnt++;//记录被调用次数
m_psl = new Singleton;//会自动调用构造函数
}
return m_psl;//返回创建出的实例的指针
}
/*打印测试信息*/
static void printS()
{
cout<<cnt<<"\tSingleton test\n"<<endl;
}
};
/*类的静态成员变量--需要在类的外面和主函数的外面进行初始化*/
Singleton * Singleton::m_psl = NULL;
int Singleton::cnt = 0;
/*子线程函数*/
void thread_task(int id)
{
//cout<<"i am thread"<<endl;
cout<<id<<"\ti am thread\n";
Singleton::getInstance()->printS();//最终会调用singleton的构造函数,并打印测试信息
}
/*主线程*/
int main()
{
thread t[3];//创建三个子线程对象
for(int i = 0;i < 3;i++)
{
t[i] = thread(thread_task,i);//为每一个子线程匹配线程函数和参数
}
cout<<"Launched from the main"<<endl;//主线程的内容和其他三个子线程的内容执行顺序会不确定
for (int i = 0; i < 3; i++)
{
t[i].join();//等待子线程任务结束才结束主线程
}
return 0;
}
运行结果:
可见构造函数被调用了3次,创建了该类的3个实例,并不是说的只创建1个类的实例!!
懒汉式单例模式的多线程改进
借助全局互斥量实现多线程下的懒汉式单例模式!
#include<iostream>
#include <thread>
#include <unistd.h>
#include <thread>
#include <mutex>
using namespace std;
mutex g_mutex;//借助互斥锁和二次检查--保证了多线程下的懒汉式单例模式
/*单例模式的类--这个类的实例只有一个--整个程序中该类的构造函数只应该被调用一次*/
class Singleton
{
private:
static Singleton * m_psl;//2. 定义一个静态指针,指向本类的变量
static int cnt;//静态变量,记录构造函数被调用的次数
/*1. 构造函数私有化*/
Singleton()
{
cout<<"constructor begin\n"<<endl;
sleep(2);//睡眠2秒钟--睡眠过程中会有其他线程进入构造函数
cout<<"constructor end\n"<<endl;
}
public:
/*全局静态函数--释放内存*/
static void freeInstance()
{
if(m_psl != NULL)
{
delete m_psl;
m_psl = NULL;
}
}
/*3. 提供一个全局的静态方法--类的全局访问点*/
static Singleton *getInstance()
{
if(m_psl == NULL)//进行判断是否有线程已经创建类的实例
{
g_mutex.lock();//上锁--在这里会导致其他没有获取互斥量的线程阻塞
if(m_psl == NULL)//二次检查--不二次检查的话,阻塞的线程一旦获取CPU的使用权便会继续创建类的实例!
{
cnt++;//记录被调用次数
m_psl = new Singleton;//会自动调用构造函数
}
g_mutex.unlock();//解锁
}
return m_psl;//返回创建出的实例的指针
}
/*打印测试信息*/
static void printS()
{
cout<<cnt<<"\tSingleton test\n"<<endl;
}
};
/*类的静态成员变量--需要在类的外面和主函数的外面进行初始化*/
Singleton * Singleton::m_psl = NULL;
int Singleton::cnt = 0;
/*子线程函数*/
void thread_task(int id)
{
//cout<<"i am thread"<<endl;
cout<<id<<"\ti am thread\n";
Singleton::getInstance()->printS();//最终会调用singleton的构造函数,并打印测试信息
}
/*主线程*/
int main()
{
thread t[3];//创建三个子线程对象
for(int i = 0;i < 3;i++)
{
t[i] = thread(thread_task,i);//为每一个子线程匹配线程函数和参数
}
cout<<"Launched from the main"<<endl;//主线程的内容和其他三个子线程的内容执行顺序会不确定
for (int i = 0; i < 3; i++)
{
t[i].join();//等待子线程任务结束才结束主线程
}
return 0;
}
运行结果:
可见,虽有三个子线程,但是只创建了一个类的实例!只调用了一次类的构造函数!
关于程序并发机制
程序的并发执行往往带来与时间有关的错误,甚至引发灾难性的后果。这需要引入同步机制。使用多进程与多线程时,有时需要协同两种或多种动作,此过程就称同步(Synchronization)。
引入同步机制的第一个原因是为了控制线程之间的资源同步访问,因为多个线程在共享资源时如果发生访问冲突通常会带来不正确的后果。例如,一个线程正在更新一个结构,同时另一个线程正试图读取同一个结构。结果,我们将无法得知所读取的数据是新的还是旧的,或者是二者的混合。
第二个原因是有时要求确保线程之间的动作以指定的次序发生,如一个线程需要等待由另外一个线程所引起的事件。
为了在多线程程序中解决同步问题,Windows提供了四种主要的同步对象,每种对象相对于线程有两种状态——信号状态(signal state)和非信号状态(nonsignalstate)。当相关联的同步对象处于信号状态时,线程可以执行(访问共享资源),反之必须等待。
这四种同步对象是:
(1)事件对象(Event)。事件对象作为标志在线程间传递信号。一个或多个线程可等待一个事件对象,当指定的事件发生时,事件对象通知等待线程可以开始执行。它有两种类型:自动重置(auto-reset)事件和手动重置(manual-reset)事件。
(2)临界区(Critical Section)。临界区对象通过提供一个进程内所有线程必须共享的对象来控制线程。只有拥有那个对象的线程可以访问保护资源。在另一个线程可以访问该资源之前,前一个线程必须释放临界区对象,以便新的线程可以索取对象的访问权。
(3)互斥量(Mutex Semaphore)。互斥量的工作方式非常类似于临界区,只是互斥量不仅保护一个进程内为多个线程使用的共享资源,而且还可以保护系统中两个或多个进程之间的的共享资源。也就是说互斥量是更牛逼的临界区。
(4)信号量(Semaphore)。信号量可以允许一个或有限个线程访问共享资源。它是通过计数器来实现的,初始化时赋予计数器以可用资源数,当将信号量提供给一个线程时,计数器的值减1,当一个线程释放它时,计数器值加1。当计数器值小于等于0时,相应线程必须等待。信号量是Windows98同步系统的核心。从本质上
讲,互斥量是信号量的一种特殊形式。
Windows/NT还提供了另外一种Windows95没有的同步对象:可等待定时器(Waitable Timer)。它可以封锁线程的执行,直到到达某一具体时间。这可以用于后台任务。
总结
在很多人印象中,单例模式可能是23个设计模式中最简单的一个。如果不考虑多线程,的确如此,但是一旦要在多线程中运用,那么从我们的教程中可以了解到,它涉及到很多编译器,多线程,C++语言标准等方面的内容。