Singleton在C++和Java中的运用
<!-- @page { margin: 2cm } H1 { margin-left: 0.16cm; margin-right: 0.16cm; margin-top: 0.16cm; margin-bottom: 0.16cm; background: #ffffff; color: #000000; background: #ffffff } H1.western { font-family: "Times New Roman"; font-size: 18pt } H1.cjk { font-family: "Times New Roman"; font-size: 16pt; font-style: normal; font-weight: bold } H1.ctl { font-family: "Times New Roman"; font-size: 16pt; font-weight: bold } P { margin-bottom: 0.21cm } A:link { so-language: zxx } -->
Singleton非常的有用,但通常也是烦恼之源。本文包括了个人实践中遇到的各种各样的Singleton,同时包括了C++和Java部分。
关于什么是Singleton的思考。
Singleton模式通常用于只需要一个对象生存的场合,但是这句话不是Singleton的全部意义,模式不是公式,它是可以变化的。比如:一个系统打印对象,进程内只需要一个,但是一个多线程并发访问数据库的程序,每个线程可能都需要一个连接对象,这样Singleton就意味着进程内有多个,每个线程里面有一个。但是如果允许连接多个数据库呢?很有可能就变成了每个线程内只允许有一个连着某个特定数据库的连接对象。Singleton的实现者需要提供一个全局的访问点给用户。最简单的就是静态成员函数Instance,为什么不用全局变量,因为全局变量不能阻止别人创建同类型的变量,而且也污染了全局空间(别人不能和你用一样的变量明)。所以我们要把类的构造函数变为protected。一个对象内部可以保存一个静态指针,然后在Instance函数内部实例化,并返回它。这种解决方案可以解决刚才打印机的要求。一个对象可以保存一个静态map,map每一项保存线程ID和静态对象指针,并且提供维护该map的一系列方法,这样就可以解决每个线程需要有一个对象的要求。但是这样就够了么,我还碰到一个不同的需求,要求运行时决定创建不同的对象。这时候可以在Instance函数上加上参数,通过参数来创建不同的对象。这些对象也可以派生自父Singleton类。也许每个线程允许对象数目不能超过3个,没关系,我们可以在Instance内部实现这些控制逻辑。
我想表达的是,Singleton可以有很多变种,有时候甚至让你感觉名不副实,但是这就是设计模式的魅力。我也是从一开始的教条主义到能接受很多精彩的变化,有时候甚至都不应该用它,或许很多是多态就够了
C++部分
http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf 这是一篇Scott Meyers and Andrei Alexandrescu的论文。
1)不要用静态或者全局变量来实现Singleton由于C++不能保证静态或者全局对象的构造函数的调用顺序以及析构顺序。所以如果程序中有多个用此方法实现的Singleton类,它们之间又有某种构造依赖关系和析构依赖关系,就会造成灾难性的后果。所以,只有当肯定不会有构造和析构依赖关系的情况下,这种实现才是合适的。不过,对于C++,采用另一种静态对象的方式更加简单和方便。因此这种方式基本上可以放弃了。
2)Meyers Singleton来控制构造顺序,但是不能控制析构顺序
Scott Meyer在<<Effective C++>>3rd Item4中提出了一个解决方案,当将non-local static变量移动到静态方法中成为local static变量的时候。C++保证当第一次静态方法被调用的时候,才会创建该静态变量。但是这里有一个疑问,创建顺序能够被控制了,可是析构顺序呢?我们只知道进程结束的时候,local static 变量会被析构,而且按照创建顺序的相反顺序进行。如果几个Singleton类的析构函数之间也有依赖关系,并且这种依赖顺序关系和LIFO顺序冲突,就会造成dead-reference问题。
3)Andrei在<<Modern C++ Designe>>第6章中提到的解决方案思路简单描述如下:
a.用new来分配Singleton对象,
b.每个Singleton对象都有一个整形寿命计数器,值大者寿命长
c.用一个特殊设计的数组来保存需要被销毁的Singleton对象的指针。寿命越长的总是在数组前面,寿命相同的按照创建顺序由前到后排列。
d.在std::at_exit中注册一个清理函数,该函数总是从c描述的数组中取出最后一个指针,然后调用delete。
4)支持多线程。能够控制构造和析构的顺序之后,现在考虑多线程。一般采用Double-Checked Locking模式。第一次check不用加锁,但是第二次check和创建对象必须加锁。还要注意编译器可能会优化代码,导致Double-Checked Locking模式失效。因此要使用volatile 修饰T* pInstance变量。
5)Loki最后提出了一个基于策略的SingletonHolder类,完美的解决了以上问题。注意SingletonHolder只支持一般意义的单例,也就是进程内唯一对象。SingletonHodler提供了更多的策略类来满足不同的要求。具体可以参考Loki文档或者。SingletonHolder接收三个策略类,分别用于管理创建和销毁对象,生命周期和线程策略。具体使用例子可以参考书本。
6)ACE和boost都提供了各自的解决方案。相比而言,SingletonHolder更灵活,能够面对各种情况。
SingletonHolder例子:
下载最新版源代码,在自己的C++程序中设置好include目录,并将singleton.cpp加入到makefile中。
#include <cstdlib>
#include "loki/Singleton.h"
#include <iostream>
using namespace std;
class MyClass{
public:
void ShowPtr()
{
cout << this << endl;
}
};
unsigned int GetLongevity(MyClass*){return 1;
}
/*
*
*/
int main(int argc, char** argv) {
MyClass c=Loki::SingletonHolder<MyClass,Loki::CreateUsingNew,Loki::SingletonWithLongevity>::Instance();//单线程环境下,用new和delete创建和销毁对象,用 GetLongevity定义寿命。c.ShowPtr();
return 0;
}
如果要支持多线程,应该在自己的应用程序中定义这个宏:LOKI_CLASS_LEVEL_THREADING。使用下面的代码创建:
MyClass c=Loki::SingletonHolder<
MyClass,
Loki::CreateUsingNew,
Loki::SingletonWithLongevity,
Loki::ClassLevelLockable,
Loki::Mutex
>::Instance();
既然是多线程环境下的Singleton,那么就应该使用Mutext进行同步,并且使用volatile强制从内存中读取数据。
SingletonHolder只支持标准Singleton,即进程内唯一。不支持变种的Singleton,比如每个线程只有一个对象,或者其他的情况,需要我们自己设计。
Java部分
1.public static final 成员变量,同时将构造函数变为private.客户程序直接访问成员变量即可。
2.private static final 成员变量。然后提供工程方法getInstance(),返回静态成员变量.由于工厂方法带来的灵活性,我们可以讲进程内唯一对象,变为线程内唯一对象。在修改getInstance方法的实现代码时不会影响到客户代码的调用。
3.Single类最好不要实现Serializable接口。因为每次反序列化生成的对象都是一个新的对象,打破了Singleton原则。作为补救措施,可以通过将所有成员变量声明为transient,并提供一个readResolve方法。所有成员变量都不参与序列化和反序列化的过程,并且readResolve总是返回一个Singleton对象。实际上这些动作也就表明不再支持序列化。因此最佳选择是不要实现Serializable接口。
4.单元素枚举类型。使用方法类似1,很简单。但是同时也实现了Serializable接口,并且也能自动防止反序列化生成新对象。如果不考虑线程,这是比前面三中都好的方式。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
5.延迟创建(按需创建或称懒汉模式)Google公司的工程师Bob Lee写的新的懒汉单例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){
}
public Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
在加载singleton时并不加载它的内部类SingletonHolder,而在调用getInstance()时调用SingletonHolder时才加载SingletonHolder,从而调用singleton的构造函数,实例化singleton,从而达到lazy loading的效果。6.Double-checked Locking模式在Java中是不能用的。原因有两个:
一是Java编译器处于优化的原因,生成的代码会不按照我们编写的顺序。极有可能先给instance变量赋值,然后再构造对象。这样一来在多线程环境下基于instance==null做判断就会得到错误的结果。
二是Java synchrorized保证了被保护的变量总是从内存中读取数据,而不是使用寄存器缓存的数据。但是当第一个线程已经创建好了对象,其他线程再来访问的时候,由于没有进入synchronized保护的代码,因此没有重新读取内存中的数据,因此有可能获取的是旧版本的数据。
详细理由参考下面的文章以及其参考的其他文章:
html">http://dev.firnow.com/course/3_program/java/javashl/2008414/110150.html
补充:软件开发 , C++ ,