C++单例模式与多线程安全

5 篇文章 0 订阅
1 篇文章 0 订阅


用代码实现如下:

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;
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值