首页 > 代码库 > 单例模式

单例模式

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

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

有的时候,总是容易把全局变量和单例模式给弄混了,下面就剖析一下全局变量和单例模式相比的缺点

首先,全局变量呢就是对一个对象的静态引用,全局变量确实可以提供单例模式实现的全局访问这个功能,

但是,它并不能保证您的应用程序中只有一个实例,同时,在编码规范中,也明确指出,

应该要少用全局变量,因为过多的使用全局变量,会造成代码难读,

还有就是全局变量并不能实现继承(虽然单例模式在继承上也不能很好的处理,但是还是可以实现继承的)

而单例模式的话,其在类中保存了它的唯一实例,这个类,它可以保证只能创建一个实例,

同时,它还提供了一个访问该唯一实例的全局访问点。

《设计模式》一书中给出了一种很不错的实现,定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。
       单例模式通过类本身来管理其唯一实例,这种特性提供了解决问题的方法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。习惯上把这个成员函数叫做Instance(),它的返回值是唯一实例的指针。

定义如下:

 

class CSingleton { private:     CSingleton()   //构造函数是私有的     {     }     static CSingleton *m_pInstance; public:     static CSingleton * GetInstance()     {         if(m_pInstance == NULL)  //判断是否第一次调用             m_pInstance = new CSingleton();         return m_pInstance;     } }; 

 

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

 CSingleton* p1 = CSingleton :: GetInstance();CSingleton* p2 = p1->GetInstance();CSingleton & ref = * CSingleton :: GetInstance();

 

对GetInstance稍加修改,这个设计模板便可以适用于可变多实例情况,如一个类允许最多五个实例。
单例类CSingleton有以下特征:
它有一个指向唯一实例的静态指针m_pInstance,并且是私有的;
它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;
它的构造函数是私有的,这样就不能从别处创建该类的实例。
大多数时候,这样的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。可以在程序结束时调用GetInstance(),并对返回的指针掉用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用GetInstance函数。
一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):

class CSingleton { private:     CSingleton()     {     }     static CSingleton *m_pInstance;     class CGarbo   //它的唯一工作就是在析构函数中删除CSingleton的实例     {     public:         ~CGarbo()         {             if(CSingleton::m_pInstance)                 delete CSingleton::m_pInstance;         }     };     static CGarbo Garbo;  //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数 public:     static CSingleton * GetInstance()     {         if(m_pInstance == NULL)  //判断是否第一次调用             m_pInstance = new CSingleton();         return m_pInstance;     } }; 

 

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

程序运行结束时,系统会调用CSingleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。
使用这种方法释放单例对象有以下特征:
在单例类内部定义专有的嵌套类;
在单例类内定义私有的专门用于释放的静态成员;
利用程序在结束时析构全局变量的特性,选择最终的释放时机;
使用单例的代码不需要任何操作,不必关心对象的释放。
进一步的讨论

但是添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:

 

class CSingleton { private:     CSingleton()   //构造函数是私有的     {     } public:     static CSingleton & GetInstance()     {         static CSingleton instance;   //局部静态变量         return instance;     } }; 

 

使用局部静态变量,非常强大的方法,完全实现了单例的特性,而且代码量更少,也不用担心单例销毁的问题。(局部静态变量的链接性请参见《C++ Primer Plus》变量存储持续性、作用域和链接性章节)

但使用此种方法也会出现问题,当如下方法使用单例时问题来了,

Singleton singleton = Singleton :: GetInstance();
这么做就出现了一个类拷贝的问题,这就违背了单例的特性。产生这个问题原因在于:编译器会为类生成一个默认的构造函数,来支持类的拷贝。最后没有办法,我们要禁止类拷贝和类赋值,禁止程序员用这种方式来使用单例,当时领导的意思是GetInstance()函数返回一个指针而不是返回一个引用,函数的代码改为如下:

 class CSingleton { private:     CSingleton()   //构造函数是私有的     {     } public:     static CSingleton * GetInstance()     {         static CSingleton instance;   //局部静态变量         return &instance;     } }; 

 

但我总觉的不好,为什么不让编译器不这么干呢。这时我才想起可以显示的声明类拷贝的构造函数,和重载 = 操作符,新的单例类如下:

class CSingleton { private:     CSingleton()   //构造函数是私有的     {     }     CSingleton(const CSingleton &);     CSingleton & operator = (const CSingleton &); public:     static CSingleton & GetInstance()     {         static CSingleton instance;   //局部静态变量         return instance;     } }; 

 

关于Singleton(constSingleton);和 Singleton &operate = (const Singleton&);函数,需要声明成私有的,并且只声明不实现。这样,如果用上面的方式(Singleton singleton = Singleton :: GetInstance();)来使用单例时,不管是在友元类中还是其他的,编译器都是报错。不知道这样的单例类是否还会有问题,但在程序中这样子使用已经基本没有问题了。

考虑到线程安全、异常安全,可以做以下扩展

class Lock { private:            CCriticalSection m_cs; public:     Lock(CCriticalSection  cs) : m_cs(cs)     {         m_cs.Lock();     }     ~Lock()     {         m_cs.Unlock();     } };  class Singleton { private:     Singleton();     Singleton(const Singleton &);     Singleton& operator = (const Singleton &);  public:     static Singleton *Instantialize();     static Singleton *pInstance;     static CCriticalSection cs; };  Singleton* Singleton::pInstance = 0;  Singleton* Singleton::Instantialize() {     if(pInstance == NULL)     {   //double check         Lock lock(cs);           //用lock实现线程安全,用资源管理类,实现异常安全         //使用资源管理类,在抛出异常的时候,资源管理类对象会被析构,析构总是发生的无论是因为异常抛出还是语句块结束。         if(pInstance == NULL)         {             pInstance = new Singleton();         }     }     return pInstance; } 

 

之所以在Instantialize函数里面对pInstance 是否为空做了两次判断,因为该方法调用一次就产生了对象,pInstance == NULL 大部分情况下都为false,如果按照原来的方法,每次获取实例都需要加锁,效率太低。而改进的方法只需要在第一次 调用的时候加锁,可大大提高效率。

【转自】http://blog.csdn.net/hackbuteer1/article/details/7460019

补充:

 如果在一开始调用 GetInstance()时,是由两个线程同时调用的(这种情况是很常见的),注意是同时,

 (或者是一个线程进入 if 判断语句后但还没有实例化 Singleton 时,第二个线程到达,此时 singleton 还是为 null)

 这样的话,两个线程均会进入 GetInstance(),而后由于是第一次调用 GetInstance(),

 所以存储在 Singleton 中的静态变量 singleton 为 null ,这样的话,就会让两个线程均通过 if 语句的条件判断,

 然后调用 new Singleton()了,

        

public static Singleton GetInstance()         {              if (singleton == null)             {                 singleton = new Singleton();             }             return singleton;         } 

 

这样的话,问题就出来了,因为有两个线程,所以会创建两个实例,

 很显然,这便违法了单例模式的初衷了,

 那么如何解决上面出现的这个问题(即多线程下使用单例模式时有可能会创建多个实例这一现象)呢?

 其实,这个是很好解决的, 您可以这样思考这个问题: 由于上面出现的问题中涉及到多个线程同时访问这个 GetInstance(), 那么您可以先将一个线程锁定,然后等这个线程完成以后,再让其他的线程访问 GetInstance()中的 if 段语句, 比如,有两个线程同时到达 如果 singleton = null 的话,那么上面提到的问题是不会存在的,因为已经存在这个实例了,这样的话, 所有的线程都无法进入 if 语句块, 也就是所有的线程都无法调用语句 new Singleton()了, 这样还是可以保证应用程序生命周期中的实例只存在一个, 但是如果此时的 singleton == null 的话, 那么意味着这两个线程都是可以进入这个 if 语句块的,那么就有可能出现上面出现的单例模式中有多个实例的问题,此时,我可以让一个线程先进入 if 语句块,然后我在外面对这个 if 语句块加锁,对第二个线程呢,由于 if 语句进行了加锁处理,所以这个进程就无法进入 if 语句块而处于阻塞状态,当进入了 if 语句块的线程完成 new  Singleton()后,这个线程便会退出 if 语句块,此时,第二个线程就从阻塞状态中恢复,即就可以访问 if 语句块了,但是由于前面的那个线程已近创建了 Singleton 的实例,所以 singleton != null,此时,第二个线程便无法通过 if 语句的判断条件了,即无法进入 if 语句块了,这样便保证了整个生命周期中只存在一个实例,也就是只有第一个线程创建了 Singleton 实例,第二个线程则无法创建实例。

 下面就来重新改进前面 Demo 中的 Singleton 类,使其在多线程的环境下也可以实现单例模式的功能。

 

namespace Singleton {     public class Singleton     {         //定义一个私有的静态全局变量来保存该类的唯一实例         private static Singleton singleton;         //定义一个只读静态对象         //且这个对象是在程序运行时创建的         private static readonly object syncObject = new object();         /// <summary>         /// 构造函数必须是私有的         /// 这样在外部便无法使用 new 来创建该类的实例         /// </summary>        private Singleton()         {        }        /// <summary>         /// 定义一个全局访问点         /// 设置为静态方法         /// 则在类的外部便无需实例化就可以调用该方法         /// </summary>         /// <returns></returns>         public static Singleton GetInstance()         {             //这里可以保证只实例化一次             //即在第一次调用时实例化             //以后调用便不会再实例化             //第一重 singleton == null             if (singleton == null)             {                 lock (syncObject)                 {                            //第二重 singleton == null                    if (singleton == null)                     {                         singleton = new Singleton();                     }                 }             }             return singleton;         }     } }

 

 上面的就是改进后的代码,可以看到在类中有定义了一个静态的只读对象  syncObject,这里需要说明的是,为何还要创建一个 syncObject 静态只读对象呢?由于提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围,所以这个引用类型的对象总不能为 null 吧,而一开始的时候,singleton 为 null ,所以是无法实现加锁的,所以必须要再创建一个对象即 syncObject 来定义加锁的范围。

还有要解释一下的就是在 GetInstance()中,我为什么要在 if 语句中使用两次判断 singleton == null , 这里涉及到一个名词 Double-CheckLocking ,也就是双重检查锁定

为何要使用双重检查锁定呢?

考虑这样一种情况,就是有两个线程同时到达,即同时调用 GetInstance(),此时由于 singleton == null,所以很明显,两个线程都可以通过第一重的 singleton == null,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new  Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 singleton == null的话,那么第二个线程还是可以调用 new  Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。

细心的朋友一定会发现,如果我去掉第一重 singleton == null ,程序还是可以在多线程下完好的运行的,考虑在没有第一重 singleton == null 的情况下,当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton(),当第一个线程退出 lock 语句块时, singleton 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重 singleton == null 挡在外面,而无法执行 new Singleton(),所以在没有第一重 singleton == null 的情况下,也是可以实现单例模式的?那么为什么需要第一重 singleton == null呢?

这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了,而如果没有第一重 singleton == null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 singleton == null 的话,那么就只有在第一次,也就是 singleton ==null 成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。

 下面将要介绍的是懒汉式单例和饿汉式单例

 懒汉式单例

 何为懒汉式单例呢,可以这样理解,单例模式呢,其在整个应用程序的生命周期中只存在一个实例, 懒汉式呢,就是这个单例类的这个唯一实例是在第一次使用 GetInstance()时实例化的,如果您不调用 GetInstance()的话,这个实例是不会存在的,即为 null, 形象点说呢,就是你不去动它的话,它自己是不会实例化的,所以可以称之为懒汉。其实呢,我前面在介绍单例模式的这几个 Demo 中都是使用的懒汉式单例,

 看下面的 GetInstance()方法就明白了:

        

 public static Singleton GetInstance()         {             if (singleton == null)             {                 lock (syncObject)                 {                     if (singleton == null)                     {                         singleton = new Singleton();                     }                 }             }             return singleton;         }

 

 从上面的这个 GetInstance()中可以看出这个单例类的唯一实例是在第一次调用 GetInstance()时实例化的,所以此为懒汉式单例。

 饿汉式单例

 上面介绍了饿汉式单例,到这里来理解懒汉式单例的话,就容易多了,懒汉式单例由于人懒,所以其自己是不会主动实例化单例类的唯一实例的,而饿汉式的话,则刚好相反,其由于肚子饿了,所以到处找东西吃,人也变得主动了很多,所以根本就不需要别人来催他实例化单例类的为一实例,其自己就会主动实例化单例类的这个唯一类。

  C# 中,可以用特殊的方式实现饿汉式单例,即使用静态初始化来完成饿汉式单例模式

 下面就来看一看饿汉式单例类

 

namespace Singleton {     public sealed class Singleton     {         private static readonly Singleton singleton = new Singleton();         private Singleton()         {         }         public static Singleton GetInstance()         {             return singleton;         }     } }

 

 要先在这里提一下的是使用静态初始化的话,无需显示地编写线程安全代码,C# 与 CLR 会自动解决前面提到的懒汉式单例类时出现的多线程同步问题。上面的饿汉式单例类中可以看到,当整个类被加载的时候,就会自行初始化 singleton 这个静态只读变量。而非在第一次调用 GetInstance()时再来实例化单例类的唯一实例,所以这就是一种饿汉式的单例类。 

好,到这里,就真正的把单例模式介绍完了,在此呢再总结一下单例类需要注意的几点:

一、单例模式是用来实现在整个程序中只有一个实例的。

二、单例类的构造函数必须为私有,同时单例类必须提供一个全局访问点。

三、单例模式在多线程下的同步问题和性能问题的解决。

四、懒汉式和饿汉式单例类。

五、C# 中使用静态初始化实现饿汉式单例类。

 

 

单例模式