设计模式(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

相关文章

在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 不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素

《x86汇编语言:从实模式到保护模式》视频来了

《x86汇编语言:从实模式到保护模式》视频来了 很多朋友留言,说我的专栏《x86汇编语言:从实模式到保护模式》写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节。 毕竟我不是作者,只有作者的解读才是最权威的。 当初我学习这本书的时候,只能靠自己摸索,网上搜不到什么好资源。 如果你正在学这本书或者汇编语言,那你有福气了。 本书作者李忠老师,以此书为蓝本,录制了全套视频。 试

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

springboot实战学习(1)(开发模式与环境)

目录 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 (3)前端 二、开发模式 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 Validation:做参数校验Mybatis:做数据库的操作Redis:做缓存Junit:单元测试项目部署:springboot项目部署相关的知识 (3)前端 Vite:Vue项目的脚手架Router:路由Pina:状态管理Eleme

状态模式state

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/state 在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。 在状态模式中,player.getState()获取的是player的当前状态,通常是一个实现了状态接口的对象。 onPlay()是状态模式中定义的一个方法,不同状态下(例如“正在播放”、“暂停

软件架构模式:5 分钟阅读

原文: https://orkhanscience.medium.com/software-architecture-patterns-5-mins-read-e9e3c8eb47d2 软件架构模式:5 分钟阅读 当有人潜入软件工程世界时,有一天他需要学习软件架构模式的基础知识。当我刚接触编码时,我不知道从哪里获得简要介绍现有架构模式的资源,这样它就不会太详细和混乱,而是非常抽象和易

使用Spring Boot集成Spring Data JPA和单例模式构建库存管理系统

引言 在企业级应用开发中,数据库操作是非常重要的一环。Spring Data JPA提供了一种简化的方式来进行数据库交互,它使得开发者无需编写复杂的JPA代码就可以完成常见的CRUD操作。此外,设计模式如单例模式可以帮助我们更好地管理和控制对象的创建过程,从而提高系统的性能和可维护性。本文将展示如何结合Spring Boot、Spring Data JPA以及单例模式来构建一个基本的库存管理系统