设计模式:单例模式(Singleton)

设计模式:单例模式(Singleton)

单例模式(Singleton)属于创建型模式(Creational Pattern)的一种。

创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。

模式动机

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。

关键代码:构造函数是私有的。

应用实例:

  1. 一个班级只有一个班主任。
  2. Windows 是多进程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  3. 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
  4. 一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号,否则会出现主键重复,因此该主键编号生成器必须具备唯一性,可以通过单例模式来实现。

在以下情况下可以使用单例模式:

  1. 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  3. 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。

模式定义

单例模式(Singleton)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类称为单例类,提供一个全局访问点来访问该实例,不需要实例化该类的对象。

注意:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

模式结构

在这里插入图片描述

时序图

在这里插入图片描述

模式实现

类的实例化方式有两种:构造函数、拷贝构造函数。我们将其设置为私有方法,便可以防止其在类外创建实例化对象。

将构造函数设置为私有方法,并不会影响静态成员在类外创建,因为他们同属于一个类域,可以访问类的所有成员。

饿汉模式

不管需不需要用,提前创建好对象 instance。

Singleton.h:

class Singleton
{
private:
	static Singleton instance; // 声明静态成员变量
	Singleton() {}
	Singleton(const Singleton& singleton) = delete; // 删除拷贝构造函数
	Singleton& operator=(const Singleton& singleton) = delete; // 删除赋值构造函数

public:
	~Singleton() {}
	// 静态方法访问静态成员
	static Singleton* getInstance()
	{
		return &instance;
	}
	// 成员函数
	void singletonOperation();
};

Singleton.cpp:

#include "Singleton.h"
#include <iostream>

// 在 C++ 中,静态成员变量需要在某个地方进行定义和初始化,以便链接器能够找到它。如果你只声明了静态成员变量而没有提供定义,就会出现以下错误:
// error LNK2001: 无法解析的外部符号 "private: static class Singleton Singleton::instance" (?instance@Singleton@@0V1@A)
Singleton Singleton::instance;

void Singleton::singletonOperation()
{
	std::cout << "singletonOperation" << std::endl;
}

上述代码通过静态成员 instance 实现单例类,原理就是函数的静态变量生命周期随着进程结束而结束。

这种方式比较常用,但容易产生垃圾对象。

懒汉模式 - 线程不安全

用的时候再通过 new 申请。

Singleton.h:

class Singleton
{
private:
	static Singleton* pInstance; // 声明静态成员变量
	Singleton() {}
	Singleton(const Singleton&) = delete; // 删除拷贝构造函数
	Singleton& operator=(const Singleton&) = delete; // 删除赋值构造函数

public:
	~Singleton()
	{
		delete pInstance;
	}
	static Singleton* getInstance()
	{
		if (pInstance == nullptr) // 判断是否第一次调用
		{
			pInstance = new Singleton();
		}
		return pInstance;
	}
	// 成员函数
	void singletonOperation();
};

Singleton.cpp:

#include "Singleton.h"
#include <iostream>

// 在 C++ 中,静态成员变量需要在某个地方进行定义和初始化,以便链接器能够找到它。如果你只声明了静态成员变量而没有提供定义,就会出现以下错误:
// error LNK2001: 无法解析的外部符号 "private: static class Singleton Singleton::instance" (?instance@Singleton@@0V1@A)
Singleton* Singleton::pInstance = nullptr;

void Singleton::singletonOperation()
{
	std::cout << "singletonOperation" << std::endl;
}

getInstance() 使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。

在懒汉式的单例类中,其实有两个状态,单例未初始化和单例已经初始化。假设单例还未初始化,有两个线程同时调用getInstance方法,这时执行 pInstance == nullptr 肯定为真,然后两个线程都初始化一个单例,最后得到的指针并不是指向同一个地方,不满足单例类的定义了,所以懒汉式的写法会出现线程安全的问题!在多线程环境下,要对其进行修改。

懒汉模式 - 线程安全

引入互斥锁,实现对getInstance内临界区的互斥访问。采用DCLP(double-check-locking-pattern)模式,使得不必每次调用都需要加锁,提高了效率。

Singleton.h:

#include <mutex>
class Singleton
{
private:
	static Singleton* instance; // 声明静态成员变量
	int value;
	static std::mutex _mutex; // 互斥锁,用于线程安全

	Singleton(int x);
	Singleton(const Singleton&) = delete; // 删除拷贝构造函数
	Singleton& operator=(const Singleton&) = delete; // 删除赋值构造函数

public:
	~Singleton();
	static Singleton* getInstance(int);
	// 成员函数
	void singletonOperation();
	void printAddr() const;
	void printValue() const;
};

Singleton.cpp:

#include "Singleton.h"
#include <iostream>
using namespace std;
// 在 C++ 中,静态成员变量需要在某个地方进行定义和初始化,以便链接器能够找到它。如果你只声明了静态成员变量而没有提供定义,就会出现以下错误:
// error LNK2001: 无法解析的外部符号 "private: static class Singleton Singleton::instance" (?instance@Singleton@@0V1@A)
Singleton* Singleton::instance = nullptr;
// 初始化互斥锁
mutex Singleton::_mutex;

Singleton::Singleton(int x = 0) : value(x) {
	cout << "Create Singleton" << endl;
}

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

Singleton* Singleton::getInstance(int x = 0)
{
	if (instance == nullptr) // 判断是否第一次调用
	{
		_mutex.lock();
		//lock_guard<mutex> lock(mutex_);
		if (instance == nullptr)
		{
			instance = new Singleton(x);
		}
		_mutex.unlock();
	}
	return instance;
}



void Singleton::singletonOperation()
{
	cout << "singletonOperation" << endl;
}

void Singleton::printAddr() const
{
	cout << "address is " << this << endl;
}

void Singleton::printValue() const
{
	std::cout << "value is " << value << std::endl;
}

单例模式在单线程环境下的测试

测试代码 main.cpp:

#include <iostream>
#include <stdlib.h>
#include "Singleton.h"
using namespace std;

int main()
{
    Singleton* sg = Singleton::getInstance();
    sg->singletonOperation();

    cout << "s1 addr is " << Singleton::getInstance() << endl;
    cout << "s2 addr is " << Singleton::getInstance() << endl;


    system("pause");
    return 0;
}

饿汉模式

运行结果:

在这里插入图片描述

程序首先取得了一个实例,使用了它的一个成员函数。才取两次实例,分别打印它们的地址,可以看到地址相同,说明实例是唯一的。

懒汉模式

测试函数 main.cpp 和之前的一样,结果也一样。

测试结果

单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。

无论饿汉还是懒汉,在单线程环境下都是一心一意的痴汉。两种实现在单线程下都是安全的。

那么,多线程呢?

单例模式在多线程环境下的测试

测试代码 main.cpp:

#include <iostream>
#include <stdlib.h>
#include <thread>
#include "Singleton.h"
using namespace std;

void func1()
{
	Singleton* s1 = Singleton::getInstance(1);
	s1->printAddr();
	s1->printValue();
}

void func2()
{
	Singleton* s2 = Singleton::getInstance(2);
	s2->printAddr();
	s2->printValue();
}

int main()
{
	thread t1(func1); 
	thread t2(func2);
	t1.join();
	t2.join();
	system("pause");
	return 0;
}

饿汉模式

运行结果:

在这里插入图片描述

懒汉模式 - 线程不安全

运行结果:

在这里插入图片描述

懒汉模式 - 线程安全

运行结果:

在这里插入图片描述

测试结果

饿汉模式在多线程环境下也是安全的。

但线程不安全的懒汉模式出现了错误,加上互斥锁以后才能在多线程环境下正确运行。

单例模式的优缺点

优点:

  1. 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  2. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  3. 避免对资源的多重占用(比如写文件操作)。
  4. 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点:

  1. 没有接口,不能继承。由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  3. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值