3.5 Singleton

单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出,GUI应用必须是单鼠标,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘。

 

单例模式有许多种实现方法,在C++中,甚至可以直接用一个全局变量做到这一点,但这样的代码显的很不优雅。使用全局对象能够保证方便地访问实例,但是不能保证只声明一个对象——也就是说除了一个全局实例外,仍然能创建相同类的本地实例。

《设计模式》一书中给出了一种很不错的实现,定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

单例模式通过类本身来管理其唯一实例,这种特性提供了解决问题的方法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。习惯上把这个成员函数叫做Instance(),它的返回值是唯一实例的指针。

 

定义如下:

#ifndef _CSINGLETON_H
#define _CSINGLETON_H

#include <cstdlib>
#include <iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton()
	{
		cout<<"开始构造一个CSingleton对象"<<endl;
	}
	static CSingleton* pInstance;
public:
	~CSingleton()
	{
		cout<<"析构一个CSingleton对象"<<endl;
	}
	static CSingleton* GetInstance()
	{
		if (pInstance==NULL)
			pInstance = new CSingleton();
		return pInstance;
	}
};
CSingleton* CSingleton::pInstance = NULL;
#endif

用户访问唯一实例的方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。这是一种防弹设计——所有GetInstance()之后的调用都返回相同实例的指针:

#include "CSingleton.h"
#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
	{
		CSingleton *p1 = CSingleton::GetInstance();
		cout<<p1<<endl;
		CSingleton *p2 = CSingleton::GetInstance();
		cout<<p2<<endl;
		CSingleton *p3 = p2->GetInstance();
		cout<<p3<<endl;

	}
	
	system("pause");
	return 0;
}

对GetInstance稍加修改,这个设计模板便可以适用于可变多实例情况,如一个类允许最多五个实例。

 

单例类CSingleton有以下特征:

它有一个指向唯一实例的静态指针m_pInstance,并且是私有的;

它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;

它的构造函数是私有的,这样就不能从别处创建该类的实例。

 

大多数时候,这样的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?

如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。

可以在程序结束时调用GetInstance(),并对返回的指针掉用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用GetInstance函数。

一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。

我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):

#ifndef _CSINGLETON_H
#define _CSINGLETON_H

#include <cstdlib>
#include <iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton()
	{
		cout<<"开始构造一个CSingleton对象"<<endl;
	}
	static CSingleton* pInstance;
	class CGarbo //它的唯一工作就是在析构函数中删除CSingleton的实例
	{
	public:
		CGarbo()
		{
			cout<<"创建CGarbo实例"<<endl;
		}
		~CGarbo()
		{
			cout<<"析构CGarbo实例"<<endl;
			if( pInstance )
				delete pInstance;
		}
	};
	static CGarbo Garbo; //定义一个静态成员,程序结束时,系统会自动调用它的析构函数
public:
	~CSingleton()
	{
		cout<<"析构一个CSingleton对象"<<endl;
	}
	static CSingleton* GetInstance()
	{
		if (pInstance==NULL)
			pInstance = new CSingleton();
		return pInstance;
	}
};
CSingleton* CSingleton::pInstance = NULL;
CSingleton::CGarbo CSingleton::Garbo;
#endif

类CGarbo被定义为CSingleton的私有内嵌类,以防该类被在其他地方滥用。

程序运行结束时,系统会调用CSingleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。

使用这种方法释放单例对象有以下特征:

在单例类内部定义专有的嵌套类;

在单例类内定义私有的专门用于释放的静态成员;

利用程序在结束时析构全局变量的特性,选择最终的释放时机;

使用单例的代码不需要任何操作,不必关心对象的释放。

 

优化Singleton类,使之适用于单线程应用

Singleton使用操作符new为唯一实例分配存储空间。因为new操作符是线程安全的,在多线程应用中你可以使用此设计模板,但是有一个缺陷:就是在应用程序终止之前必须手工用delete摧毁实例。否则,不仅导致内存溢出,还要造成不可预测的行为,因为Singleton的析构函数将根本不会被调用。而通过使用本地静态实例代替动态实例,单线程应用可以很容易避免这个问题。下面是与上面的GetInstance()稍有不同的实现,这个实现专门用于单线程应用:

CSingleton* CSingleton :: GetInstance()
{
static CSingleton inst;
return &inst;
}

本地静态对象实例inst是第一次调用GetInstance()时被构造,一直保持活动状态直到应用程序终止,指针m_pInstance变得多余并且可以从类定义中删除掉,与动态分配对象不同,静态对象当应用程序终止时被自动销毁掉,所以就不必再手动销毁实例了。

 

 

说明:

下面简单说下单例模式的使用中需要注意的一些问题。

1.     实例指针一定要设为静态吗?

因为GetInstance这个方法要用到该实例指针,且GetInstance这个方法是static的,所以这个指针必须是static的,否则GetInstance无法访问该实例指针。以此同时保证了向其他对象提供唯一的同一个内存区的实例指针。

2.     为什么不弃用懒汉式而直接用饿汉式?

首先,懒汉式是典型的以时间换取空间的例子,就是每次获取实例时都要进行判断,看是否要创建实例,浪费判断时间。当然如果一直没有人用的话,就不会创建实例,则是节约空间。而饿汉式是典型的以空间换取时间,就是说当类装载的时候,就创建出一个实例,不管你用不用它,然后每次调用时就不用判断了,节省了运行时间。

这里说某种方式一定比另一种方式好,它们两者各有各的优势。关键取决于你在时间和空间上效率的取舍。

3.     单例模式只是为了节省资源吗?

首先要说明的是,在一些情况下使用单例模式是可以达到节省资源的目的,但是单例模式的意图不只是为了节省资源,如果仅仅为了节省资源就使用单例模式的话可能造成单例模式的滥用。单例模式是为了确保在整个应用期间只有一个实例,以达到用户的特定的使用目的。比如windows操作系统里,有多个线程要同时进行文件创建、打开、修改一个文件的操作时,就用到单例模式设计文件管理器。所有的文件操作都必须同个这个唯一的实例来进行文件操作,避免的混乱的情况。

4.     单例模式的坏处?

 扩展困难,由于GetInstance静态函数没有办法生成子类的实例。如果要拓展,只有重写那个类。

  隐式使用引起类结构不清晰。比如有时候,你并不知道某个类A是单例类,当你读类B的时候,你可能先看它头文件,或者类视图里的内容,从这里你无法知道A和B 关系,因为B类在实现的时候才使用A类的那个所谓的GetInstance函数,读不到这行,你就会知道B类对A类的依赖关系。

 导致程序内存泄露的问题。很多人只是调用了GetInstance生成唯一的实例,却永远new被封装在GetInstance里忘了去释放内存。

5.     什么情况下不能用单例模式?

单例模式简单易用,但是也是所有设计模式中最容易滥用的模式。当你的类想得到很好的扩展时,不能使用单例模式。



另外有一篇好文章:

http://blog.csdn.net/liushuijinger/article/details/9069801

单例模式大家并不陌生,也都知道它分为什么懒汉式、饿汉式之类的。但是你对单例模式的理解足够透彻吗?今天我带大家一起来看看我眼中的单例,可能会跟你的认识有所不同。

下面是一个简单的小实例:

public class Singleton {  

	//单例实例变量 
	private static Singleton instance = null;  

	//私有化的构造方法,保证外部的类不能通过构造器来实例化 
	private Singleton() {}  

	//获取单例对象实例 
	public static Singleton getInstance() {  

		if (instance == null) {   
			instance = new Singleton();   
		}  

		System.out.println("我是简单懒汉式单例!");  
		return instance;  
	}  
}  

很容易看出,上面这段代码在多线程的情况下是不安全的,当两个线程进入if (instance == null)时,两个线程都判断instance为空,接下来就会得到两个实例了。这不是我们想要的单例。

 

接下来我们用加锁的方式来实现互斥,从而保证单例的实现。

//同步法懒汉式 
public class Singleton {  

	//单例实例变量 
	private static Singleton instance = null;  

	//私有化的构造方法,保证外部的类不能通过构造器来实例化 
	private Singleton() {}  

	//获取单例对象实例 
	public static synchronized  Singleton getInstance() {  

		if (instance == null) {   
			instance = new Singleton();   
		}  

		System.out.println("我是同步法懒汉式单例!");  
		return instance;  
	}  
}  

加上synchronized后确实保证了线程安全,但是这样就是最好的方法吗?很显然它不是,因为这样一来每次调用getInstance()方法是都会被加锁,而我们只需要在第一次调用getInstance()的时候加锁就可以了。这显然影响了我们程序的性能。我们继续寻找更好的方法。

 

经过分析发现,只需要保证instance = new Singleton()是线程互斥就可以保证线程安全,所以就有了下面这个版本:

//双重锁定懒汉式 
public class Singleton {  

	//单例实例变量 
	private static Singleton instance = null;  

	//私有化的构造方法,保证外部的类不能通过构造器来实例化 
	private Singleton() {}  

	//获取单例对象实例 
	public static Singleton getInstance() {  
		if (instance == null) {   
			synchronized (Singleton.class) {  
				if (instance == null) {   
					instance = new Singleton();   
				}  
			}  
		}  
		System.out.println("我是双重锁定懒汉式单例!");  
		return instance;  
	}  
}  

这次看起来既解决了线程安全问题,又不至于每次调用getInstance()都会加锁导致降低性能。看起来是一个完美的解决方案,事实上是这样的吗?

很遗憾,事实并非我们想的那么完美。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制导致了双重检查加锁方法的失效。这个问题的关键在上面代码上的第5行:instance = new Singleton(); 这行其实做了两个事情:1、调用构造方法,创建了一个实例。2、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法之前,instance已经被设置为非空了。下面我们一起来分析一下:

 

假设有两个线程A、B

1、线程A进入getInstance()方法。

2、因为此时instance为空,所以线程A进入synchronized块。

3、线程A执行 instance =new Singleton(); 把实例变量instance设置成了非空。(注意,是在调用构造方法之前。)

4、线程A退出,线程B进入。

5、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并不是Singleton的实例,因为没有调用构造方法。)

6、线程B退出,线程A进入。

7、线程A继续调用构造方法,完成instance的初始化,再返回。

 

难道就没有一个好方法了吗?好的方法肯定是有的,我们继续探索!

//双重锁定懒汉式 
public class Singleton {  

	//单例实例变量 
	private static Singleton instance = null;  

	//私有化的构造方法,保证外部的类不能通过构造器来实例化 
	private Singleton() {}  

	//获取单例对象实例 
	public static Singleton getInstance() {  
		if (instance == null) {   
			synchronized (Singleton.class) {  
				if (instance == null) {   
					instance = new Singleton();   
				}  
			}  
		}  
		System.out.println("我是双重锁定懒汉式单例!");  
		return instance;  
	}  
}  

1、线程A进入getInstance()方法。

2、因为instance是空的 ,所以线程A进入位置//1的第一个synchronized块。

3、线程A执行位置//2的代码,把instance赋值给本地变量temp。instance为空,所以temp也为空。 

4、因为temp为空,所以线程A进入位置//3的第二个synchronized块。(后来想想这个锁有点多余)

5、线程A执行位置//4的代码,把temp设置成非空,但还没有调用构造方法!(“无序写”问题) 

6、如果线程A阻塞,线程B进入getInstance()方法。

7、因为instance为空,所以线程B试图进入第一个synchronized块。但由于线程A已经在里面了。所以无法进入。线程B阻塞。

8、线程A激活,继续执行位置//4的代码。调用构造方法。生成实例。

9、将temp的实例引用赋值给instance。退出两个synchronized块。返回实例。

10、线程B激活,进入第一个synchronized块。

11、线程B执行位置//2的代码,把instance实例赋值给temp本地变量。

12、线程B判断本地变量temp不为空,所以跳过if块。返回instance实例。

 

到此为止,上面的问题我们是解决了,但是我们突然发现为了解决线程安全问题,但给人的感觉就像身上缠了很多毛线.... 乱糟糟的,所以我们要精简一下:

//饿汉式 
public class Singleton {  

	//单例变量,static的,在类加载时进行初始化一次,保证线程安全  
	private static Singleton instance = new Singleton();      

	//私有化的构造方法,保证外部的类不能通过构造器来实例化。      
	private Singleton() {}  

	//获取单例对象实例      
	public static Singleton getInstance() {  
		System.out.println("我是饿汉式单例!");  
		return instance;  
	}  
}  

看到上面的代码,瞬间觉得这个世界清静了。不过这种方式采用的是饿汉式的方法,就是预先声明Singleton对象,这样带来的一个缺点就是:如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。

 

到底有没有完美的方法呢?继续看:

//内部类实现懒汉式 
public class Singleton {  

	private static class SingletonHolder{  
		//单例变量   
		private static Singleton instance = new Singleton();  
	}  

	//私有化的构造方法,保证外部的类不能通过构造器来实例化。 
	private Singleton() {  

	}  

	//获取单例对象实例 
	public static Singleton getInstance() {  
		System.out.println("我是内部类单例!");  
		return SingletonHolder.instance;  
	}  
}  

懒汉式(避免上面的资源浪费)、线程安全、代码简单。因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。

 

简单说一下上面提到的无序写,这是jvm的特性,比如声明两个变量,String a; String b; jvm可能先加载a也可能先加载b。同理,instance= new Singleton();可能在调用Singleton的构造函数之前就把instance置成了非空。这是很多人会有疑问,说还没有实例化出Singleton的一个对象,那么instance怎么就变成非空了呢?它的值现在是什么呢?想了解这个问题就要明白instance = new Singleton();这句话是怎么执行的,下面用一段伪代码向大家解释一下:

mem = allocate();             //为Singleton对象分配内存。
instance = mem;               //注意现在instance是非空的,但是还没有被初始化。 
ctorSingleton(instance);    //调用Singleton的构造函数,传递instance.

由此可见当一个线程执行到instance = mem; 时instance已为非空,如果此时另一个线程进入程序判断instance为非空,那么直接就跳转到return instance;而此时Singleton的构造方法还未调用instance,现在的值为allocate();返回的内存对象。所以第二个线程得到的不是Singleton的一个对象,而是一个内存对象。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值