设计模式(2) - Singleton单件模式

2023-12-08 01:58

本文主要是介绍设计模式(2) - Singleton单件模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  1. 基本实现

  2. 静态成员的释放

  2.1. 使用内嵌类

  2.2. 使用局部静态变量

  2.3. 堆内存释放问题

  3. 线程安全问题

  3.1 问题起源

  3.2. 线程安全实现方案

  3.2.1 Lazy initialization

  3.2.2 Eager Initialization

 4. 总结

  4.1 相互引用

  4.2 多线程环境下

  4.3 多实例析构


    最近项目里边用到了ACE, 里边的很多类都使用了ACE_Singleton(爱立信不少遗留项目,都喜欢用这种比较heavy的组件 :-()。
    单件模式的原理虽然简单,易懂,但感觉要用好它,也不是那么容易。下面从几个方面来进行说明。

 

  1. 基本实现

  下面代码是单件模式的最基本的实现,仅限于单线程环境。 通过定义一个静态的成员,来保存这个唯一的对象实例。并通过一个静态接口来获取这个唯一的实例。

class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}static Singleton* getInstance(){if(NULL==_instance){_instance = new Singleton();}return _instance;}
private:Singleton(){cout<<"Singleton Constructor"<<endl;};static Singleton* _instance;
};Singleton* Singleton::_instance = NULL;int main()
{Singleton* sgn1 = Singleton::getInstance();Singleton* sgn2 = Singleton::getInstance();if(sgn1 == sgn2)cout<<"unique instance"<<endl;system("pause");return 0;
}

  运行结果为:
  Singleton constructor
  unique instance
  Press any key to continue . . .

  2. 静态成员的释放

  有了new, 就应该有对应的delete. 那到底什么时候释放呢。因为new分配的是堆内存,估计即使加上析构函数,也没有效果。带着这个疑问,做了下实验,给类Singleton加上了一个析构函数。

Singleton::~Singleton()
{  std::cout<<"Singleton destructor"<<std::endl;
}

  实验结果显示推测正确,堆内存不会自动释放。
  自然的,会想到定义一个释放函数的方法,调用delete Singleton::instance()对静态实例进行释放。但是,这样增加了风险,因为调用者很可能会忘记调了此函数。
  调研了些资料,其实有下面的几种方法:

  2.1. 使用内嵌类

  通过一个内嵌类和一个静态成员来实现自动释放的机制,相当于为单件加了个垃圾回收器。关键点在于static Cleaner clr;这个声明,由于是静态成员,系统会在栈里分配内存,回收工作也就由系统自动完成了。

class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}static Singleton* getInstance(){if(NULL==_instance){_instance = new Singleton();static Cleaner clr;}return _instance;}
private:Singleton(){cout<<"Singleton Constructor"<<endl;};static Singleton* _instance;class Cleaner{public:Cleaner(){cout<<"Cleaner Constructor"<<endl;}~Cleaner(){cout<<"Cleaner Destructor"<<endl;if(NULL!=Singleton::_instance)delete Singleton::_instance;}};};Singleton* Singleton::_instance = NULL;int main()
{Singleton* sgn1 = Singleton::getInstance();Singleton* sgn2 = Singleton::getInstance();if(sgn1 == sgn2)cout<<"unique instance"<<endl;system("pause");return 0;
}

  运行结果为:
  Singleton Constructor
  Cleaner Constructor
  unique instance
  Press any key to continue . . . 

  按任意键退出后,观察到有下面打印:
  Cleaner Destructor
  Singleton Destructor

  2.2. 使用局部静态变量

  前面提到的方法,需要新增一个嵌套类,增加了复杂度。
  可以对第1节(基本实现)中的getInstance方法进行优化,返回一个局部静态变量。

 static Singleton* getInstance(){static Singleton instance;return &instance;}

  程序在结束的时候,系统会自动回收所有的静态/全局内存,这样,单件模式中的静态成员就能被析构掉了。

  2.3. 堆内存释放问题

  对于第1节(基本实现)中,单例是通过new方法分配的。可能有人会疑问:进程退出时,os会自动回收进程所占用的各种资源,包括栈内存,堆内存,代码指令等等。那为何还会存在第2节所说的资源未释放问题呢?进程退出时,os不就自动释放了么?
  其实,有下面这些因素需要考虑:
  --现代的大部分os,如windows, linux的确会自动释放,但在一些嵌入式os中,不是自动回收的。
  --如果代码是内核级的一些驱动,它造成的内存错误,也是无法自动回收的。除非重启os。
  --良好的编程习惯约束,本程序分配的所有资源,都应该由程序自己去析构或者释放。这个c++开发者的最基本素养。

  3. 线程安全问题

 

  3.1 问题起源

  所谓线程安全,就是说如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  基于第1节中的基本实现,来验证下它是否线程安全:

#include<Windows.h>
#include<process.h>
#include<iostream>using namespace std;class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}void DoSomething(){cout<<"Singleton DoSomething"<<endl;}static Singleton* getInstance(){if(NULL == _instance)_instance = new Singleton();return _instance;}
private:Singleton(){static int count = 0;cout<<"Singleton Constructor:"<<++count<<endl;};static Singleton* _instance;
};unsigned __stdcall thread( void* )
{Singleton* p = Singleton::getInstance();p->DoSomething();return 0;
}Singleton* Singleton::_instance = NULL;int main( int argc, void* argv[] )
{for( int i = 0; i < 10; ++i ){HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );CloseHandle(t);}system("pause");return 0;
}

  运行结果为(注意:下面看似格式有些错乱,其实是程序的原始输出,并不是排版问题):

Singleton Constructor:1
Singleton DoSomething
Singleton Constructor:5Singleton Constructor:3
Singleton DoSomething
Singleton Constructor:4
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton Constructor:6
Singleton DoSomething
Singleton Constructor:2
Singleton DoSomething

Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Press any key to continue

从上面结果可以看到,前面的实现方案是非线程安全的。构造函数被调用了多次,生成了多个实例。

  3.2. 线程安全实现方案

  3.2.1 Lazy initialization

  也称为慢初始化方法。即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。
  此种方法,需要用锁来保证其线程安全性。因为如上3.1所示,多个线程可能会同时进入到判断实例是否存在的if语句中,它是非线程安全的。

  方案一:Double-Check

  可以使用double-check来保证线程安全,这也是ACE_Singleton采用的方法。但是如果处理大量数据时,该锁可能会成为严重的性能瓶颈。
  实现原理如下:

  Singleton* Singleton::getInstance(){if(NULL == _instance){Lock();if(NULL == _instance)_instance = new Singleton();Unlock();}return _instance;}
基于这个原理,重新实现了单件模式的基本代码,加入了线程互斥机制:
#include<Windows.h>
#include<process.h>
#include<iostream>using namespace std;class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}void DoSomething(){cout<<"Singleton DoSomething"<<endl;}static Singleton* getInstance(){if(NULL == _instance){WaitForSingleObject(mtx, INFINITE);if(NULL == _instance)_instance = new Singleton();ReleaseMutex(mtx);}return _instance;}static HANDLE mtx;
private:Singleton(){static int count = 0;cout<<"Singleton Constructor:"<<++count<<endl;};static Singleton* _instance;
};unsigned __stdcall thread( void* )
{Singleton* p = Singleton::getInstance();p->DoSomething();return 0;
}Singleton* Singleton::_instance = NULL;
HANDLE Singleton::mtx=INVALID_HANDLE_VALUE;int main( int argc, void* argv[] )
{Singleton::mtx = CreateMutex(NULL, FALSE, (LPCWSTR)"Mutex");if(NULL==Singleton::mtx)return 0;for( int i = 0; i < 10; ++i ){HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );CloseHandle(t);}Sleep(100);CloseHandle(Singleton::mtx);system("pause");return 0;
}
运行结果:
Singleton Constructor:1
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething

Press any key to continue . . .

  方案二:内部静态实例

  此方法,就是2.2中所介绍的方法的改进。实现原理如下:

Singleton* Singleton::getInstance()
  {
    Lock();
    static Singleton instance;
    Unlock();
    return &instance;
}
  这里需要注意的是,C++0x以后,要求编译器保证内部静态变量的线程安全性,可以不加锁。但C++0x以前,仍需要加锁。这里是无锁版本:
 
#include<Windows.h>
#include<process.h>
#include<iostream>using namespace std;class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}void DoSomething(){cout<<"Singleton DoSomething"<<endl;}static Singleton* getInstance(){static Singleton instance;return &instance;}
private:Singleton(){cout<<"Singleton Constructor"<<endl;};static Singleton* _instance;
};unsigned __stdcall thread( void* )
{Singleton* p = Singleton::getInstance();p->DoSomething();return 0;
}Singleton* Singleton::_instance = NULL;int main( int argc, void* argv[] )
{for( int i = 0; i < 5; ++i ){HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );CloseHandle(t);}Sleep(10);system("pause");return 0;
}
  运行结果为:
  Singleton Constructor
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Press any key to continue . . .

  3.2.2 Eager Initialization

  也称为急切初始化。即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。
  静态初始化实例保证了其线程安全性。因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
  在性能需求较高时,可以使用这种模式,避免频繁的锁争夺。实现如下:
 
#include<Windows.h>
#include<process.h>
#include<iostream>using namespace std;class Singleton
{
public:~Singleton(){cout<<"Singleton Destructor"<<endl;}void DoSomething(){cout<<"Singleton DoSomething"<<endl;}static Singleton* getInstance(){if(NULL == _instance)_instance = new Singleton();return _instance;}
private:Singleton(){cout<<"Singleton Constructor"<<endl;};static Singleton* _instance;
};unsigned __stdcall thread( void* )
{Singleton* p = Singleton::getInstance();p->DoSomething();return 0;
}Singleton* Singleton::_instance = new Singleton;int main( int argc, void* argv[] )
{for( int i = 0; i < 5; ++i ){HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );CloseHandle(t);}Sleep(10);system("pause");return 0;
}
  运行结果如下:
  Singleton Constructor
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Singleton DoSomething
  Press any key to continue . . .

 4. 总结

  使用单件模式时,需要注意的几个方面

  4.1 相互引用

  任意两个 Singleton 类的构造函数不能相互引用对方的实例, 否则会导致程序崩溃. 如:

  SingletonA& SingletonA::Instance() {const SingletonB& b = SingletonB::Instance();static SingletonA theSingleton;return theSingleton;}SingletonB& SingletonB::Instance() {const SingletonA & b = SingletonA::Instance();static SingletonB theSingleton;return theSingleton;}

  4.2 多线程环境下

  在多线程的应用场合下必须小心使用。如果唯一实例尚未创建时, 有两个线程同时调用创建方法, 且它们均没有检测到唯一实例的存在, 便会同时各自创建一个实例, 这样就有两个实例被构造出来, 从而违反了单例模式中实例唯一的原则。 解决办法即第3节所讲。

  4.3 多实例析构

  多个 Singleton 实例相互引用的情况下, 需要谨慎处理析构函数. 如: 初始化顺序为 SingletonA » SingletonB » SingletonC 的三个 Singleton 类, 其中 SingletonA SingletonB 的析构函数调用了 SingletonC 实例的成员函数, 程序退出时, SingletonC 的析构函数 将首先被调用, 导致实例无效, 那么后续 SingletonA SingletonB 的析构都将失败, 导致程序异常退出。

这篇关于设计模式(2) - Singleton单件模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/468126

相关文章

Nginx location匹配模式与规则详解

《Nginxlocation匹配模式与规则详解》:本文主要介绍Nginxlocation匹配模式与规则,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、环境二、匹配模式1. 精准模式2. 前缀模式(不继续匹配正则)3. 前缀模式(继续匹配正则)4. 正则模式(大

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

SpringBoot如何通过Map实现策略模式

《SpringBoot如何通过Map实现策略模式》策略模式是一种行为设计模式,它允许在运行时选择算法的行为,在Spring框架中,我们可以利用@Resource注解和Map集合来优雅地实现策略模式,这... 目录前言底层机制解析Spring的集合类型自动装配@Resource注解的行为实现原理使用直接使用M

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3

大数据spark3.5安装部署之local模式详解

《大数据spark3.5安装部署之local模式详解》本文介绍了如何在本地模式下安装和配置Spark,并展示了如何使用SparkShell进行基本的数据处理操作,同时,还介绍了如何通过Spark-su... 目录下载上传解压配置jdk解压配置环境变量启动查看交互操作命令行提交应用spark,一个数据处理框架

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

模版方法模式template method

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法

【iOS】MVC模式

MVC模式 MVC模式MVC模式demo MVC模式 MVC模式全称为model(模型)view(视图)controller(控制器),他分为三个不同的层分别负责不同的职责。 View:该层用于存放视图,该层中我们可以对页面及控件进行布局。Model:模型一般都拥有很好的可复用性,在该层中,我们可以统一管理一些数据。Controlller:该层充当一个CPU的功能,即该应用程序

迭代器模式iterator

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/iterator 不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素