用代码实现如下:
class Singleton {
public:
static Singleton* CreateObj() {
if (m_pInstance == NULL) {
m_pInstance = new Singleton();
}
return m_pInstance;
}
void test() {
cout << "访问单例类的接口。" << endl;
}
~Singleton(){ }
private:
Singleton() { }
static Singleton* m_pInstance;
};
Singleton* Singleton::m_pInstance = NULL; //静态成员需要初始化!!!
如上所示为一个简单的单例类,由于私有成员m_pInstance为静态成员,即全局共享,需要初始化。并且,m_pInstance 指向本类的唯一实例,由公有函数CreateObj( )返回。
静态成员函数的调用形式可为:
Singleton *p = Singleton::CreateObj(); //创建实例
上述代码在单线程下是安全的,但在多线程下是不安全的。原因是:多线程环境下 ,每一个线程都会调用CreateObj( )函数构建实例,又由于所有线程共享全局资源,m_pInstance为静态成员(即全局共享),会导致构造多个实例,但是最终只有一个实例有效,即m_pInstance只能指向一个实例,导致资源浪费。明显这不符合单实例模式的要求。
考虑到多线程下单实例模式的安全性,我们可以有以下做法:
①直接实例化
线程不安全问题主要是由于 m_pInstance 被实例化多次,采取直接实例化 m_pInstance 的方式就不会产生线程不安全问题。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
private:
static Singleton* m_pInstance = new Singleton();
上述代码直接将静态成员变量m_pInstance初始化即实例化为一个对象,那么全局便共享这一个实例。但是会一直占用资源(即便在不需要这个实例的条件下)导致资源浪费。
②加锁
1、低效式:
只需要对CreateObj( ) 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 m_pInstance。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 m_pInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。
Linux下互斥锁加锁方法为:pthread_mutex_lock(&g_lock); 注意加锁前要先调用pthread_mutex_init进行初始化,这里不再赘述。
public:
static Singleton* CreateObj() {
pthread_mutex_lock(&g_lock); //Linux下互斥锁
if (m_pInstance == NULL) {
m_pInstance = new Singleton();
}
pthread_mutex_unlock(&g_lock); //解锁
return m_pInstance;
}
上述代码有个坏处,虽然能保证每次只有一个线程能够拿到锁,但是当第一次创建出实例之后,后续线程只需要直接返回实例即可,已经不需要再加锁解锁了。但是代码里面还是会进行加锁解锁,浪费资源和时间。
2、高效式(双重校验)
先给出代码:
public:
static Singleton* CreateObj() {
if (m_pInstance == NULL) { //未被实例化
pthread_mutex_lock(&g_lock); //加锁
if(m_pInstance == NULL){
m_pInstance = new Singleton();
}
pthread_mutex_unlock(&g_lock); //解锁
}
return m_pInstance;
}
考虑上面实现,在m_pInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 m_pInstance = new Singleton( ); 这条语句,只是先后的问题,那么就会进行两次实例化。(这个问题在下面解释)
因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 m_pInstance 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 m_pInstance == null 时两个线程同时进行实例化操作。
这么理解:一开始的时候:
static Singleton* CreateObj() {
if (m_pInstance == NULL) {
pthread_mutex_lock(&g_lock); //加锁
/* 如果不加这一个判定
if(m_pInstance == NULL){
m_pInstance = new Singleton();
}
*/
m_pInstance = new Singleton();
pthread_mutex_unlock(&g_lock); //解锁
}
return m_pInstance;
}
有两个线程都进入了CreateObj( )函数,并且都通过了if(p_Instance == NULL),因为此时还未实例化。那么有一个线程拿到了锁,它就可以去创建对象。那这个时候另一个线程怎么办?另一个线程就停留在加锁语句前面,等第一个线程创建完对象之后放锁,第二个线程才可以继续往下走,也就是拿锁、创建对象。但是!此时第一个线程已经创建完实例了,所以已经不能再创建了,再继续创建就会浪费了。因此需要在拿锁之后,再加一句判定if(m_pInstance == NULL) { m_pInstance = new Singleton( ); }。来防止被锁在外面的第二个线程 在放锁之后还要创建实例。
上述问题一开始比较难理解,多琢磨就可以了。通过双重校验的方式,可以做到单例模式下的多线程安全。
最后,再给出一个《effective C++》中另一种实现方式,更加直接,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。
并且,C++11之后,要求编译器保证内部静态变量的线程安全性,也可以不用加锁解锁,下面的实现是带互斥锁的。
class single{
private:
static pthread_mutex_t lock;
single(){
pthread_mutex_init(&lock, NULL);
}
~single(){}
public:
static single* getinstance();
};
pthread_mutex_t single::lock;
single* single::getinstance(){
pthread_mutex_lock(&lock);
static single obj; //直接创建静态实例并返回
pthread_mutex_unlock(&lock);
return &obj;
}