设计模式之单例模式

 

 

一、引子

 

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

1.        单窗体的问题。

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

     

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

 

2. 大对象问题

 

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

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();        
        }

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

 

三、      单例模式

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_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++各个版本的实现和自己的理解,如有错误,请大家指正。

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

 

posted @ 2014-09-18 00:10  霜天雪舞  阅读(1913)  评论(4编辑  收藏  举报