设计模式之单例模式

 

 

1、引子

 

首先来看两个常见的问题:程序员

1.        单窗体的问题。

在主应用程序菜单点击菜单,弹出工具箱窗体,如今的问题是,但愿工具箱要么不出现,出现也只能够出现一个,可是实际上每次点击菜单,都会实例化一个“工具箱”并显示出来,这样会产生不少个“工具箱”,不是所但愿的。注意这里但愿的是“工具箱”窗体单例,而不是进程单个实例(进程单个实例:例如PC上已经打开一个迅雷,再次运行迅雷,结果并无再开一个迅雷而仍是以前的,区分同一PC登录多个QQ客户端)。数据库

     

如上图,每次单击菜单都会实例化一个工具箱窗体,与指望不符。编程

 

2. 大对象问题

 

对象有保存对象状态信息的一些字段,字段过多或者字段自己占据大量内存,都会致使对象过大。下面看一段示例:windows

class SimpleLargeObject
    {
        private const int NUM = 100 * 1024 * 1024;//100MB
        private byte[] data = 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();
            }
        }

  复制代码潜在的问题也是很明显的:

  1. 一份代码多出重复,若是需求变化或者有BUG时就须要改多个地方。若是有5个地方须要实例化“工具箱”窗体,这个小bug就须要改动5个地方,可见复制粘贴多么害人。
  2. 复制粘贴是最容易的编程,也是最没有价值的编程,只求达到目标,如何能有提升。

上面的程序就有潜在的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();        
        }

这样一来,客户端再也不考虑是否须要去实例化的问题,而把责任都给了应该负责的类去处理。这就是一个很根本的设计模式:单例模式

 

3、      单例模式

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判断,代码更加精炼,又能避免加锁解锁。

 

4、      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;

P一、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_pInstance
Singleton::GC Singleton::m_gc;//这里初始化Singleton里面嵌套类GC的静态成员m_gc

int _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机制

 

RAIIResource 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_pInstance
Singleton::GC Singleton::m_gc;//这里初始化Singleton里面嵌套类GC的静态成员m_gc
std::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_gc
int _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++各个版本的实现和本身的理解,若有错误,请你们指正。

在实际的开发中,并不会用到单例模式的这么多种版本,每一种设计模式,都应该在最适合的场合下使用,在往后的项目中,应作到有地放矢,而不能为了使用设计模式而使用设计模式。

相关文章
相关标签/搜索