您的位置:首页 > 其它

单例模式与双重检查方案

2009-08-01 22:45 239 查看
本文链接:单例模式与双重检查方案

­
一个类在全局只有一个实例的应用方式,即为单例模式。单例模式一般应用在全局性的工具之类的类上,如一个程序中的各种文件管理器(这个有可能进化成多例模式),或者是一些工具类(其实一些简单的单例类,写成纯静态类都可以,效果差不多的-_-!)。实现方式主要是单例类自己维护一个静态的自己类对象的引用,而使用这个类的时候通过一个静态方法获得那唯一的一个实例。代码见后面的例子。

双重检查,为了提高性能而使用的一个小技巧。 以下面的例子为例。为了保证多线程中的调用安全性,对获得实例的方法GetInstance()内部进行了线程同步(lock(object))。但是锁的开销要比if语句大得多,因而有如下改进方案,首先检查instance是否为空,不为空的话就不用同步,直接返回instance,如果为空,进入加锁语句块,为了避免刚才检查instance之后有另一个线程创建了instance的实例,所以在同步块中再次检查,而这次可以确保不会有其他线程正执行在能创建instance的地方,如果的确有其他线程在之前短暂的时间内加了进来创建了instance,那么就退出同步块,返回已经创建的instance,否则自己创建。这样当instance已经实例化后,GetInstance()方法就不会进行线程同步。这就是所谓的双重检查。

­
单例模式有许多种具体的实现方式,这里是一种为了保证线程安全性而使用双重检查的实现。

C#代码如下: (Java几乎一样,除了关键字)

class Singleton {
private static Singleton instance;
private Singleton() {
// your own code here
}
public static Singleton GetInstance() {
// first check.
if (instance == null) {
// Notice, more than one threads might be here.
lock (typeof(Singleton)) { // synchronize
// second check after locked the object: class of Singleton.
if (instance == null) {
instance = new Singleton();
}
}
}

return instance;
}
// other methods
}
­
很完美吧?只有instance没有初始化之前才需要同步,instance一旦有了对象,就不会进入同步语句了,可以节省以后获得实例的时间。

不过,先别急,问题没有结束呢。

查看资料后,发现这里有一个问题,在.Net与Java中,这通常不能保证结果的正确!!(C++好像是可以的,不确定)

原因在于其执行顺序,instance = new Singleton();在.Net与Java中不保证构造在引用赋值之前执行!从而可能引用到未构造完成的对象。也就是说这样虽然能保证大多数情况下的正确,但不是全部!而如果因为这个地方造成了程序出错,估计你很难想到是哪里出问题了。

(网上这样的例子很多,详细的也不少,感兴趣的话大家肯以看看)

­
那就没有改进的办法了么?

先看这样:

把instance = new Singleton();改为:

Singleton temp = new Singleton();

instance = temp;

怎么样?这样是不是可以确保instance引用的一定是初始化过的对象?

看起来很完美。

实际上呢?别忘了编译器是干什么的。优化啊,优化。一优化,上面那两种写法不就一样了?

那么换成这样呢?

Singleton temp = new Singleton();

int i = 0;

i += 2;

instance = temp;

编译器的优化会做类似与这样的动作:为了提高执行速度,可能把代码前后没有逻辑关联的的语句调换位置。结果就不知道谁在谁前面执行了……而且i这个没有任何意义的变量相关动作会被优化掉,等于和上面的一样了。

­
总有办法的吧?应该有吧,大家都可以想想。我提供一个比较垃圾的方法(不保证正确,仅提供个参考,还请大家多提意见):

首先加一个Method:

private Singleton m(Singleton s) {

return s != null ? s : new Singleton(); // no use at all, 's' unable to be null.

}

然后把instance = new Singleton();改为:

Singleton temp = new Singleton();

instance = temp.m(temp);

这样的话,只要方法m不被内联,就可以保证instance的赋值绝对可以按照顺序执行。(我不知道.Net里面的自动内联机制是怎么处理的。Java不清楚。而且这样的方法,就算内联了,执行顺序应该也不会被优化乱了)

­
其实在.Net与Java里面有更好、更简单的、效率更高的单例模式的实现方法,看代码。推荐使用下面的方法。

C#代码: (这里为了与C++、Java更接近而使用了方法。在.Net中更推荐使用属性(property)来完成getter与setter,这样代码更简洁,使用更方便。)

class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// your own code here
}
private static Singleton GetInstance() {
return instance;
}
// other methods
}
­
怎么样,简单吧。而且连if语句都可以省掉了。和上面的单例模式实现方法相比,唯一的缺点在于这种单例模式的单例类只要一加载(.Net/Java),实例就被创建,而不是第一次用到的时候。

­
在C++中稍微麻烦些。

C++代码:

class Singleton {
static Singleton* instance;
private:
Singleton();
virtual ~Singleton();
// other methods
public:
static Singleton* GetInstancePtr();
static Singleton& GetInstanceRef();
};
Singleton* Singleton::instance = new Singleton();
Singleton::Singleton() {
Console::WriteLine("In Singleton's Constructor.");
}
Singleton::~Singleton() {
Console::WriteLine("In Singleton's Destructor.");
}
Singleton* Singleton::GetInstancePtr() {
return instance;
}
Singleton& Singleton::GetInstanceRef() {
return *instance;
}

­
然后可以这样简单的测试下:

Singleton* instance1 = Singleton::GetInstancePtr();
Singleton& instance2 = Singleton::GetInstanceRef();
Console::WriteLine("instance1 == instance2 : " + (instance1 == &instance2));
­

我想会有人问,只有new而没有delete?这不是内存泄漏么?这样没事吗?是的,是没有delete,也确实没事。

单例模式,单例嘛,整个内存中就这一个,泄漏也就这一个。而且单例模式的用意就是整个程序不论何时使用这个类,只有这一个实例,也就是要从头用到尾了,所以说这个实例占用的内存就不用管了,在程序退出的时候交给操作系统去做善后吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: