首页 > 代码库 > 设计模式之单例模式
设计模式之单例模式
一、引子
首先来看两个常见的问题:
1. 单窗体的问题。
在主应用程序菜单点击菜单,弹出工具箱窗体,现在的问题是,希望工具箱要么不出现,出现也只可以出现一个,但是实际上每次点击菜单,都会实例化一个“工具箱”并显示出来,这样会产生很多个“工具箱”,不是所希望的。注意这里希望的是“工具箱”窗体单例,而不是进程单个实例(进程单个实例:例如PC上已经打开一个迅雷,再次运行迅雷,结果并没有再开一个迅雷而还是之前的,区分同一PC登陆多个QQ客户端)。
如上图,每次单击菜单都会实例化一个工具箱窗体,与期望不符。
2. 大对象问题
对象有保存对象状态信息的一些字段,字段过多或者字段本身占据大量内存,都会导致对象过大。下面看一段示例:
class SimpleLargeObject { private const int NUM = 100 * 1024 * 1024;//100MB private byte[] data = http://www.mamicode.com/null; public SimpleLargeObject() { data = new byte[NUM]; for (int i = 0; i < data.Length; i++) { data[i] = (byte)(i % 255); } } public void Method1() { Console.WriteLine("Method1"); } // other methods.... } class Program { static void Main(string[] args) { SimpleLargeObject obj1=new SimpleLargeObject(); obj1.Method1(); Console.WriteLine("Press enter to create a new object..."); Console.ReadLine(); SimpleLargeObject obj2 = new SimpleLargeObject(); obj2.Method1(); Console.ReadLine(); } }
为了更体现出问题,这里夸张一点,SimpleLargeObject占据内存100MB。
运行发现内存占据100MB,按回车键继续创建另外一个对象,此时内存翻倍增加至200MB… 可以想象,当特定环境下需要产生无数个对象,而这些对象本身的状态信息由私有字段来维护,字段的取值不同会影响到公开方法的行为,而这些对象又不需要在同一时刻都要存在,或者无数个这样的对象状态信息无关紧要,产生这么多对象会导致内存占用过多。
对于第一个问题,常规解决方法是在调用窗体类中声明一个ToolBoxForm类型的全局,判断这个ToolBoxForm类型的全局变量是否实例化过就行了。
private ToolBoxForm toolBoxForm = null; private void toolStripMenuItemToolBox_Click(object sender, EventArgs e) { if (toolBoxForm == null) { toolBoxForm = new ToolBoxForm(); toolBoxForm.Show(); } }
这样似乎解决问题了。
新需求来了:现在不但要在菜单里面启动“工具箱”,还需要在“工具栏”上的按钮来快捷启动“工具箱”。菜单栏有些常用的功能提供快捷按钮再正常不过的需求了。
这个不难,增加一个工具栏控件,然后添加onclick事件,复制同样的代码就行了:
private void toolStripButton1_Click(object sender, EventArgs e) { if (toolBoxForm == null) { toolBoxForm = new ToolBoxForm(); toolBoxForm.Show(); } }
复制代码潜在的问题也是很明显的:
- 一份代码多出重复,如果需求变化或者有BUG时就需要改多个地方。如果有5个地方需要实例化“工具箱”窗体,这个小bug就需要改动5个地方,可见复制粘贴多么害人。
- 复制粘贴是最容易的编程,也是最没有价值的编程,只求达到目标,如何能有提高。
上面的程序就有潜在的Bug,启动“工具箱”,然后把“工具箱”窗体关闭,再点启动按钮,问题就暴露出来了。原因是关闭“工具箱”窗体时,它的实例并没有变为null,而只是Disposed。
Form.Show()方法出的窗体,关闭调用Close()会Dispose内存,对象销毁,但指向对象的引用不为null;
Form.ShowDilog()方法出的窗体,关闭窗体不会释放对象的内存,窗体的引用也不为null,窗体只是hidden而已。
上述Bug修复,并重构提炼方法后的代码:
private ToolBoxForm toolBoxForm = null; private void toolStripMenuItemToolBox_Click(object sender, EventArgs e) { OpenToolBox(); } private void toolStripButton1_Click(object sender, EventArgs e) { OpenToolBox(); } private void OpenToolBox() { if (toolBoxForm == null||toolBoxForm.IsDisposed) { toolBoxForm = new ToolBoxForm(); toolBoxForm.Show(); } }
现在基本没什么问题了。
二 .类的职责
在上面几步的优化和改善,已经基本没什么问题了,但是这样做“工具箱”是否实例化都是在调用显示“工具箱”的地方来判断,这样不符合逻辑,主窗体里面应该只是通知启动“工具箱”,至于“工具箱”窗体是否实例化过,主窗体根本不关心,这不属于主窗体的职责,“工具箱”是否实例化过,应该有“工具箱”自己来判断。对象是否实例化是它自己的责任,而不是别人的责任,别人只是使用它就可以了。
对象的实例化其实就是new的过程,如果要控制对象的实例化由该类自身来维护,那么类的构造函数应该是私有的,这样外部就不能用new来实例化它了,而让这个类只能实例化一次,用静态的类变量能达到目的,因为静态是该类型共享的,而该类型刚好是这个类本身。
客户端使用的代码:
private void toolStripMenuItem1_Click(object sender, EventArgs e) { ToolBoxForm.Instance.Show(); } private void toolStripButton1_Click(object sender, EventArgs e) { ToolBoxForm.Instance.Show(); }
这样一来,客户端不再考虑是否需要去实例化的问题,而把责任都给了应该负责的类去处理。这就是一个很根本的设计模式:单例模式。
三、 单例模式
1. 基本的单例
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。——GOF的《设计模式:可复用面向对象软件的基础》
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且可以提供一个访问该实例的方法。
class Singleton { private static Singleton instance; private Singleton() //构造方法为private,这就堵死了外界利用new创建此类型实例的可能 { } public static Singleton GetInstance() //次方法是获得本类实例的唯一全局访问点 { if (instance == null) { instance = new Singleton(); } return instance; } } class Program { static void Main(string[] args) { // Singleton s0 = new Singleton();//错误,外界不能通过new来创建此类型实例 Singleton s1 = Singleton.GetInstance(); Singleton s2 = Singleton.GetInstance(); if (s1 == s2) { Console.WriteLine("两个对象是相同的实例"); } Console.ReadLine(); } }
运行结果,s1和s2是同一个实例,都是通过唯一的全局访问点Singleton.GetInstance()方法返回的。
2. 多线程环境下的单例
先模拟一个多线程的环境:
class Singleton { private static Singleton instance; private Singleton() //构造方法为private,这就堵死了外界利用new创建此类型实例的可能 { Thread.Sleep(50);//此处模拟创建对象耗时 } public static Singleton GetInstance() //次方法是获得本类实例的唯一全局访问点 { if (instance == null) { instance = new Singleton(); } return instance; } } class Program { const int THREADCOUNT = 200; static List<Singleton> sList = new List<Singleton>(THREADCOUNT); static object objLock = new object(); static void Main(string[] args) { Task[] tasks=new Task[THREADCOUNT]; for (int i = 0; i < THREADCOUNT; i++) { tasks[i] = Task.Factory.StartNew(ThredFunc); } Task.WaitAll(tasks);//确保所有任务执行完毕 Console.WriteLine("sList.Count:" + sList.Count); int index1 = -1; int index2 = -1; if(HasDifferentInstance(out index1,out index2)) { Console.WriteLine("含有不相同的实例,index1={0},index2={1}", index1, index2); } Console.WriteLine("执行完毕."); Console.ReadLine(); } private static bool HasDifferentInstance(out int index1,out int index2) { index1 = index2 = -1; for (int i = 0; i < sList.Count; i++) { for (int j = i + 1; j < sList.Count - 1; j++) { if (sList[i] != sList[j]) { index1 = i; index2 = j; return true; } } } return false; } private static void ThredFunc() { Singleton singleton = Singleton.GetInstance(); lock (objLock) { sList.Add(Singleton.GetInstance()); } }
我们在Singleton的构造函数延迟50ms来模拟创建对象耗时,这样在多线程的环境下,很容易出现在一个线程执行Singleton.GetInstance()时创建对象,而这个对象的创建理论上是要消耗时间的,在创建对象之前instance为null,还未返回,此时另一个线程也执行Singleton.GetInstance()判断instance为null,执行了new创建了对象,这样出现了对象实例不为同一个对象的情况。
为了解决这个问题,在执行new创建实例的地方加上锁,同时在锁定之前判断下是否为null,这样如果已经创建就不用进入锁了。
public static Singleton GetInstance() //次方法是获得本类实例的唯一全局访问点 { if (instance == null) { lock (objLock) { if (instance == null) { instance = new Singleton(); } } } return instance; }
对于instance存在的情况,就直接返回;当instance为null并且同时有两个线程GetInstance()方法时,它们都可以通过第一重instance==null的判断,然后由于lock机制,这两个线程则只有一个进入,另一个在排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有了第二重的instance是否为null的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,所以需要两次判断。
进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。但是,这种实现方法在平时的项目开发中用的很好,也没有什么问题?但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈;为此,一种新的单例模式的实现也就出现了。
上面的Doule-Check Locking(双重锁定) 能进一步优化,利用CLR类型构造器保证线程安全:
class Singleton { private static Singleton instance; static Singleton() //类型构造器,确保线程安全 { instance = new Singleton(); } private Singleton() //构造方法为private,这就堵死了外界利用new创建此类型实例的可能 { Thread.Sleep(50);//此处模拟创建对象耗时 } public static Singleton GetInstance() //次方法是获得本类实例的唯一全局访问点 { return instance; } }
不需要null判断,代码更加精炼,又能避免加锁解锁。
四、 C++ 单例模式
尽管单例模式的思想是一致的,但是C++ 与C#有很多不同点,甚至有时候用到语言平台的独有特性有意想不到的效果,例如利用CLR的特性,类型构造器能确保线程安全性。这里介绍一下C++实现单例模式。 利用GOF中单例模式的定义,很容易写出如下的代码:
版本一:
class Singleton{private: Singleton() { } static Singleton * m_pInstance;public: static Singleton * GetInstance() { if (m_pInstance == NULL) { m_pInstance = new Singleton(); } return m_pInstance; }};Singleton * Singleton::m_pInstance = NULL;
用户访问唯一实例的方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的,所有GetInstance()之后的调用都返回相同实例的指针:
Singleton *p1 = Singleton::GetInstance();
Singleton *p2 = Singleton::GetInstance(); Singleton *p3 = p2;
P1、p2都是通过GetInstance()全局访问点访问的,指向的是同一实例,p3是经过指针赋值,也是指向同一实例,它们的地址相同:
大多数时候,这样的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?这样会不会导致内存泄漏呢?
我们一般的编程观念是,new操作是需要和delete操作进行匹配的;是的,这种观念是正确的。具体看场景。static Singleton * m_pInstance;m_pInstance 指针本身为静态的,存储方式为静态存储,生命周期为进程周期;而其指向的实例对象在堆上分配,这个堆对象有个特点就是只有一个实例,堆内存由程序员释放或程序结束时可能由OS回收。
堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
注意,这里是可能。具体能不能得看OS,目前windows是可以的,而嵌入式系统有些是不能的。所以还得看场景。
在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,尽管这个指向实例的指针为静态的,而这个实例为堆中对象并且只有一个,进程结束后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。而针对服务端程序,一般是长期运行,但是这个实例也只有一个,进程结束,操作系统会回收内存。
显然,把内存回收的责任交给OS,虽然大多数情况下是没问题的,但是还是看场景的,内存能不能回收也取决于OS内核。
更重要的是,在以下情形,是必须需要进行实例销毁的:
在类中,有一些文件锁了,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
版本二:添加手动释放函数
class Singleton{private: Singleton() { } static Singleton * m_pInstance ;public: static Singleton * GetInstance() { if (m_pInstance == NULL) { m_pInstance = new Singleton(); } return m_pInstance; } static void DestoryInstance() { if (m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; } }};
我们单例类中添加一个DestoryInstance()函数来删除实例,可以在进程退出之前来调用这个函数释放,结合前面“类的职责”小结,很快会发现这样不是很优雅,理想情况下是类的使用者只管拿来用,而不用关注什么时候释放,并且程序员忘了调用这个函数也是很容易发生的事。能不能实现像boost中shared_ptr<T>这样自动释放内存呢?
由于这个实例的生命周期为直到进程结束,因此可以设计一个包装类作为静态变量,静态变量的生命周期也是到进程结束销毁,可以在这个包装类的析构函数里面释放资源。
以下是改进版本:
版本三:利用RAII自动释放
class Singleton{private: Singleton() { } static Singleton * m_pInstance ; class GC //内部包装类 { public: ~GC() { if (m_pInstance != NULL) { std::cout << "Here is the test,delete m_pInstance." << std::endl; delete m_pInstance; m_pInstance = NULL; } } }; static GC m_gc;public: static Singleton * GetInstance() { if (m_pInstance == NULL) { m_pInstance = new Singleton(); } return m_pInstance; }};Singleton * Singleton::m_pInstance = NULL;//这里初始化Singleton的静态成员m_pInstanceSingleton::GC Singleton::m_gc;//这里初始化Singleton里面嵌套类GC的静态成员m_gcint _tmain(int argc, _TCHAR* argv[]){ Singleton *p1 = Singleton::GetInstance(); Singleton *p2 = Singleton::GetInstance(); std::cin.get(); return 0;}
运行程序,执行到cin.get()后敲回车,程序即将退出,输出以下结果:
说明嵌套类GC的析构函数已经执行。此处使用了一个内部GC类,而该类的作用就是用来释放资源,其定义在Singleton的private部分,外部无法访问,也不关心。程序在结束的时候,系统会自动析构所有的全局变量,实际上,系统也会析构所有类的静态成员变量,就像这些静态变量是全局变量一样。我们知道,静态变量和全局变量在内存中,都是存储在静态存储区的,所以在析构时,是同等对待的。在程序运行结束时,系统会调用Singleton的静态成员static GC m_gc的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。这里运用了C++中的RAII机制。
RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
前面的各个版本还没考虑多线程的问题,参考前面C#版本的“双检锁”,而C++语言本身不提供多线程支持的,多线程的实现是由操作系统提供支持的,可以用系统API。这里用
C++ 0x 的线程库,C++ 0x里面部分库由boost发展而来。
版本四: 多线程环境下“双检锁”
class Singleton{private: Singleton() { } static Singleton * m_pInstance; class GC //内部包装类 { public: ~GC() { if (m_pInstance != NULL) { std::cout << "Here is the test,delete m_pInstance." << std::endl; delete m_pInstance; m_pInstance = NULL; } } }; static GC m_gc; static std::mutex m_mutex;public: static Singleton * GetInstance() { if (m_pInstance == NULL) { m_mutex.lock(); if (m_pInstance == NULL) { m_pInstance = new Singleton(); } m_mutex.unlock(); } return m_pInstance; }};Singleton * Singleton::m_pInstance = NULL;//这里初始化Singleton的静态成员m_pInstanceSingleton::GC Singleton::m_gc;//这里初始化Singleton里面嵌套类GC的静态成员m_gcstd::mutex Singleton::m_mutex; //初始化Singleton静态成员m
这里使用了C++ 0x的mutex,需要#include <mutex>
继续参考之前C#版本的优化,提供静态初始化版本:
版本五:静态初始化
class Singleton{private: Singleton() { } const static Singleton * m_pInstance; class GC //内部包装类 { public: ~GC() { if (m_pInstance != NULL) { std::cout << "Here is the test,delete m_pInstance." << std::endl; delete m_pInstance; m_pInstance = NULL; } } }; static GC m_gc;public: static Singleton * GetInstance() { return const_cast<Singleton *>(m_pInstance); } void TestMethod() { std::cout << "Singleton::TestMethod" << std::endl; }};const Singleton* Singleton::m_pInstance = new Singleton(); //这里静态初始化Singleton::GC Singleton::m_gc;//这里初始化Singleton里面嵌套类GC的静态成员m_gcint _tmain(int argc, _TCHAR* argv[]){ Singleton *p1 = Singleton::GetInstance(); Singleton *p2 = Singleton::GetInstance(); p1->TestMethod(); std::cin.get(); return 0;}
因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。
语言特性
下面我们看看其它版本,先不考虑多线程(多线程问题前面讨论过了,不做重点,也可以在主函数之前以单线程方式先完成初始化来达到目的)。
class Singleton{private: Singleton() { }public: static Singleton& GetInstance() { static Singleton instance; return instance; } void TestMethod() { std::cout << "Singleton::TestMethod()" << std::endl; }};
这个版本不再使用指针,而是返回一个静态局部变量的引用。也许有人会问,返回局部变量的引用,局部变量过了作用域就析构了啊,但是注意这里是静态局部变量,存储
方式为静态存储,生命周期为到进程退出,所以不用担心函数结束就析构了。C# 和Java等没有静态局部变量的概念,这个可以说是C/C++的一个特性。
写程序测试:
int _tmain(int argc, _TCHAR* argv[]){ Singleton::GetInstance().TestMethod(); Singleton s1= Singleton::GetInstance(); Singleton s2 = s1; if (addressof(s1) == addressof(s2)) { cout << "同一实例" << endl; } else { cout << "不同实例" << endl; cout <<"s1的地址:"<<(int)(&s1) << endl; cout <<"s2的地址:" <<(int)(&s2) << endl; } std::cin.get(); return 0;}
发现s1和s2是不同的实例,这是因为对象的创建除了构造函数外还有其他方式,例如复制构造函数、赋值操作符等,都需要禁止。
改进版本:
class Singleton{private: Singleton() { } Singleton(const Singleton&) = delete;//禁止复制 Singleton operator=(const Singleton&) = delete;//禁止赋值操作public: static Singleton& GetInstance() { static Singleton instance; return instance; } void TestMethod() { std::cout << "Singleton::TestMethod()" << std::endl; }};
这样,外部企图通过赋值操作符或者复制来创建对象,都会报错:
Singleton::GetInstance() 是唯一的全局访问点和访问方式。
项目中出现多个需要用到单例的类怎么办?分别编写禁止复制构造函数、禁止赋值操作,分别编写GetInstance()方法 这种重复的工作?我们宏可以解决这个重复性工作:
#define SINGLINTON_CLASS(class_name) private: class_name(){} class_name(const class_name&); class_name& operator = (const class_name&); public: static class_name& Instance() { static class_name one; return one; }class Simple{ SINGLINTON_CLASS(Simple)public: void Print() { cout<<"Simple::Print()"<<endl; }};
可以把上面的宏写到一个头文件中,在需要写单例的地方include这个头文件,单例类开头只需加上SINGLINTON_CLASS(class_name)就行了,其中class_name为当前类名,然后可以讲工作重心放到这个类的设计上。
客户的还是照样调用:
int _tmain(int argc, _TCHAR* argv[]){ Simple::Instance().Print(); cin.get(); return 0;}
总结
单例模式可以说是设计模式里面最基本和简单的一种了,为了写这篇文章,自己调查了很多方面的资料,例如《大话设计模式》,同时加上C++各个版本的实现和自己的理解,如有错误,请大家指正。
在实际的开发中,并不会用到单例模式的这么多种版本,每一种设计模式,都应该在最适合的场合下使用,在日后的项目中,应做到有地放矢,而不能为了使用设计模式而使用设计模式。
设计模式之单例模式