单件模式确保类只有唯一的一个实例对象,并提供全局访问。

    这一句对单件模式的描述,可以再细化一下分为两点:

    1、 唯一的一个实例对象。2、该对象全局访问性质。

    可以看出,使用全局变量可以实现第二点,即程序可以在任意时刻对该对象进行访问,但并不能保证该类型的对象只有一个(因为在程序中任意处可以new出新的该类对象)。

    为了避免程序使用new操作符创建出多余的对象,因此在实现时,应该将该类型的构造函数声明为protected,这样防止了该类型成员函数以外的代码对构造函数的调用。     

View Code
 1  class Singleton
 2 {
 3 protected:
 4     Singleton(){}
 5 private:
 6      static Singleton *_instance;
 7 public:
 8       static Singleton* GetInstance(){
 9            if (!_instance){                     //1
10                 _instance = new Singleton();  //2
11            }
12             return _instance;
13       }
14 };

    类定义中,使用static Singleton* _instance指向Singele类唯一对象。使用静态类成员变量,保证了该指针的唯一型。而GetInstance()函数声明为静态类成员函数,是为了在任何时刻都可以通过Singleton::GetInstance();的调用形式来获取Singleton的类对象。在GetInstance函数中,采用了,延迟初始化技术,使得只有在需要使用到Singleton对象的时候才会对其进行初始化。避免了在程序一开头对类对象进行初始化带来的开销。但是,现在还没有到高枕无忧的地步。

    考虑到在多线程的环境下,在未对_instance进行初始化时,有多个线程同时执行到1处进入了if内,那么就将产生出多个Singleton对象。因此还需要对GetInstance函数进行小小的改动,使它具有线程安全性。最简单的办法是在GetInstance函数内部创造一个临界区:

1 static Singleton* GetInstance()
2 {
3      Lock();                  //1
4      if (!_instance){     //2
5            _instance = new Singleton();//3
6      }
7      Release();
8      return _instance;
9 }

 

如此,当_instance还未初始化时,多个线程进入GetInstance后,最先进入的线程1获得锁,进入2、3执行,在这之间,其它线程如线程2获得执行权,执到到1处,因为线程1拥有锁,所以线程2只能等待线程1初始化_instance之后,释放锁,线程2执行到2处,因为_instance已经被初始化,所以线程2不会执行到3处。这样,就实现了GetInstance的线程安全性。在多线程环境下,GetInstance也能工作正常。

但是需要考虑的是,使用临界区机制,在任一时刻,都只有一个线程能完成对GetInstance的执行操作,而当我们在完成了第一次对Singleton对象的初始化之后,其它对GetInstance的调用操作中的临界区机制就完全是多余的了,因为此时任何线程都不会再有创建Singleton对象的操作。如果对Singleton对象的访问操作对系统性能的影响较为可观的话,那么,这样的临界区设置,无疑会使系统性能大幅下降。因此需要对其进行一些改进:

1、      不采用延迟初始化机制,在程序开始运行时,就创建出Singleton对象,其后的GetInstance()只需要返回_instance指针即可,这样便避免了临界区机制带来的开销。

2、      采用双重检查锁定:

 1 static Singleton* GetInstance()
 2 {
 3     if (!_instance){         //1
 4         Lock();                //2
 5         if (!_instance){       //3
 6              _instance = new Singleton();//4
 7         }
 8         Release();
 9     }
10     return _instance;
11 }

 

在未初始化_instance时,当多个线程进入GetInstance后,最先进行的线程1执行到1,假设在线程1在执行到2时未被抢占(若被抢占,则将后述操作的线程1看作该抢占线程即可),继续执行3、4创建Singleton对象。在此之间被抢占时,因为2处,线程1拥有的锁,因此其它线程会在2处等待,当线程1完成初始化后,释放锁,其它线程进行3,因为此时_instance已完成初始化,所以不会再执行4.同时,因为1的存在,在_instance初始化完成后,再有线程进行GetInstance函数,就不会再进入临界区,这样就将临界区所带来的开销减小到对_instance的初始化阶段,在完成初始后,就没有了临界区的开销。

如上两种方法,都可以减少临界区带来的开销(第一种是根本上消除了临界区的开销)。

 

小结一下,使用单件模式,可以灵活的控制类型产生的对象,确保程序所访问的对象是该类型的唯一对象。而且,通过对Singleton稍稍修改,就可以实现使其支持数量受限的对象集合。不足是,单件模式所带来的开销,前面已经讲述过的。还有就是单件模式没有涉及到对象的销毁问题。需要注意此处可能产生的问题。

参考资料:

《设计模式——可复用面向对象软件的基础》

《Head First Design Patterns》