c++ 单例模式

单例模式用于确保类只有一个实例,并提供全局访问点。本文通过模拟Windows任务管理器,介绍了单例模式的动机、实现方法,包括饿汉式和懒汉式的讨论,并提出了一种更好的初始化延迟加载实现,最后总结了单例模式的优点、缺点和适用场景。

确保对象的唯一性——单例模式

单例模式的动机

对于一个软件系统中的某些类而言,无须创建多个实例。举个大家都熟知的例子——Windows任务管理器,如图3-1所示。可以做一个这样的尝试:在Windows任务栏的右键弹出菜单上多次单击“启动任务管理器”,看能否打开多个任务管理器窗口(注:计算机中毒或私自修改Windows内核者除外)。在正常情况下,无论启动任务管理器多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说,在一个Windows系统中,任务管理器存在唯一性。为什么要这样设计呢?可以从以下两个方面来分析:其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源(任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等),而且根本没有必要显示多个内容完全相同的窗口;其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这会给用户带来误解,更不可取。由此可见,确保Windows任务管理器在系统中有且仅有一个非常重要。
在实际开发中也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。

单例模式概述

下面来模拟实现Windows任务管理器。假设任务管理器的类名为TaskManager,在TaskManager类中包含了大量的成员方法,例如构造函数TaskManager(),显示进程的方法displayProcesses(),显示服务的方法displayServices()等,该类的示意代码如下:

class TaskManager {
public:
	TaskManager() {...}
	void displayProcesses() {...}
	void displayServices() {...}
	...
}

为了实现Windows任务管理器的唯一性,通过以下3步对TaskManager类进行重构:
(1)由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的唯一性,需要禁止类的外部直接使用new来创建对象,因此需要将TaskManager的构造函数的可见性改为private,代码如下:

private:
	TaskManager() {...}

(2)将构造函数的可见性改为private后,虽然类的外部不能再使用new来创建对象,但是在TaskManager的内部还是可以创建对象的,可见性只对类外有效。因此,可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,代码如下:

private:
	static TaskManager* tm = nullptr;

(3)为了保证成员变量的封装性,将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,代码如下:

public:
	static TaskManager* getInstance() {
		if (tm == nullptr)
			tm = new TaskManager();
		return tm;
	}

在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm==nullptr为true),则使用new关键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的tm对象。
需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便外界其他对象使用;其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建TaskManager对象。事实上,在类外也无法创建TaskManager对象,因为构造函数是私有的。

通过以上3个步骤,完成了一个最简单的单例类的设计,其完整代码如下:

class TaskManager {
private:
	TaskManager() {}
public:
	void displayProcesses() {}
	void displayServices() {}

	static TaskManager* getInstance() {
		if (tm == nullptr)
			tm = new TaskManager();
		return tm;
	}

private:
	static TaskManager* tm;
};
TaskManager* TaskManager::tm = nullptr;

在类外无法直接创建新的TaskManager对象,但可以通过代码TaskManager::getInstance()访问实例对象。第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例。

上述代码也是单例模式的一种最典型实现方式,有了以上基础,理解单例模式的定义和结构就非常容易了。单例模式定义如下:
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

负载均衡器的设计

Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,即只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。
Sunny公司开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OhBOfIe7-1659243354220)(%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F_md_files/5ba87390-0e8c-11ed-afd5-7d51594f2a39_20220728234553.jpeg?v=1&type=image&token=V1:dDgZcNBr4dPL730ypmT1KBQOIHeQ-9aLpvM3fv8DUa4)]
在图中,将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下:

#include <vector>
#include <string>
#include <cstdlib>
#include <iostream>

using namespace std;

class LoadBalancer {
private:
	LoadBalancer () {
        serverList = new vector<string>();
    }

public:
	static LoadBalancer *getLoadBalancer () {
        if (instance == nullptr)
            instance = new LoadBalancer();
        return instance;
	}

    void addServer(string server) {
        serverList->push_back(server);
    }
    void removeServer(string server) {
        for (vector<string>::iterator iter = serverList->begin(); \
            iter != serverList->end(); iter++ ) {
            if (*iter == server)
                serverList->erase(iter);
        }
    }
    string getServer() {
        int sz = serverList->size();
        return (*serverList)[rand() % sz];
    }

private:
    static LoadBalancer * instance;
    vector<string>* serverList = nullptr;
};

LoadBalancer* LoadBalancer::instance = nullptr;

编写如下客户端测试代码:

int main() {
    // LoadBalancer l1, l2, l3, l4;
    LoadBalancer * l1 = LoadBalancer::getLoadBalancer();
    LoadBalancer * l2 = LoadBalancer::getLoadBalancer();
    LoadBalancer * l3 = LoadBalancer::getLoadBalancer();
    LoadBalancer * l4 = LoadBalancer::getLoadBalancer();

    if (l1 == l2 && l2 == l3 && l3 == l4) {
        cout << "wei yi xing" << endl;
    }
    l1->addServer("S1");
    l1->addServer("S2");
    l1->addServer("S3");
    l1->addServer("S4");

    for (int i = 0; i < 10; i++) {
        string s = l2->getServer();
        cout << "fen fa qing qiu dao: " << s << endl;
    }
}

编译并运行程序,输出结果如下:

wei yi xing
fen fa qing qiu dao: S4
fen fa qing qiu dao: S3
fen fa qing qiu dao: S2
fen fa qing qiu dao: S4
fen fa qing qiu dao: S2
fen fa qing qiu dao: S4
fen fa qing qiu dao: S3
fen fa qing qiu dao: S1
fen fa qing qiu dao: S2
fen fa qing qiu dao: S2

虽然创建了4个LoadBalancer对象,但是它们实际上是同一个对象。因此,通过使用单例模式可以确保LoadBalancer对象的唯一性。

饿汉式单例与懒汉式单例的讨论

Sunny公司开发人员使用单例模式实现了负载均衡器的设计,但是在实际使用中出现了一个非常严重的问题。当负载均衡器在启动过程中用户再次启动负载均衡器时,系统无任何异常,但当客户端提交请求时出现请求分发失败。通过仔细分析发现原来系统中还是存在多个负载均衡器对象,导致分发时目标服务器不一致,从而产生冲突。为什么会这样呢?Sunny公司开发人员百思不得其解。
现在对负载均衡器的实现代码进行再次分析。当第一次调用getLoadBalancer()方法创建并启动负载均衡器时,instance对象为null值,因此系统将执行代码“instance=new LoadBalancer();”,在此过程中,由于要对LoadBalancer进行大量初始化工作,需要一段时间来创建LoadBalancer对象。而在此时,如果再一次调用getLoadBalancer()方法(通常发生在多线程环境中),由于instance尚未创建成功,仍为null值,判断条件“instance==null”为真值,因此代码“instance=new LoadBalancer();”将再次执行,导致最终创建了多个instance对象,这违背了单例模式的初衷,也导致系统发生运行错误。
如何解决该问题?至少有两种解决方案。在正式介绍这两种解决方案之前,先介绍一下单例类的两种不同实现方式——饿汉式单例类(Eager Singleton)和懒汉式单例类(Lazy Singleton)。

1.饿汉式单例类

饿汉式单例类是实现起来最简单的单例类,其结构图如图3-4所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MEBPNAnC-1659243354221)(%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F_md_files/bd0313a0-0f4c-11ed-8426-29500c829a07.jpeg?v=1&type=image)]
从图中可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载时就已经创建了单例对象,代码如下:

class EagerSingleton {
private:
	EagerSingleton() {}		//私有化构造函数
	static EagerSingleton* instance;	//定义静态指针变量(指针才不会调用构造函数)
public:
	static EagerSingleton* getInstance();	//静态成员方法
};
EagerSingleton* EagerSingleton::instance = new EagerSingleton();	//c++中非常量的静态成员变量,初始化必须在类外
 
EagerSingleton* EagerSingleton::getInstance(){
	return instance;
}

2.懒汉式单例类与线程锁定

除了饿汉式单例外,还有一种经典的懒汉式单例,也就是前面提到的负载均衡器LoadBalancer类的实现方式。懒汉式单例类结构图如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWSslT5I-1659243354221)(%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F_md_files/b053e850-0f51-11ed-8426-29500c829a07.jpeg?v=1&type=image)]

class LazySingleton {
private:
	LazySingleton() {}
	volatile static LazySingleton* instance;		//volatile将屏蔽系统优化
	static mutex mtx;		//信号量,用于给代码块上锁
public:
	volatile static LazySingleton* getInstance();
};
volatile LazySingleton* LazySingleton::instance = nullptr;
mutex LazySingleton::mtx;
volatile LazySingleton* LazySingleton::getInstance(){
	if (instance == nullptr) {	
		lock_guard<mutex> lock(mtx);	//锁定整个函数会极大程度的影响效率,因为只有一部分数据可以通过上面的if判定
		if (instance == nullptr) {
			instance = new LazySingleton();
		}
	}	
	return instance;
}

3.饿汉式单例类与懒汉式单例类比较

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载。但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的概率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

一种更好的单例实现方法

饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题。有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是肯定的。下面来学习这种更好的被称为Initialization on Demand Holder(IoDH)的技术。
实现IoDH时,需在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下:

class Singleton {
private:
	Singleton(){}
	struct Holder {
		static Singleton* instance;
	};
public:
	static Singleton* getInstance();
};
Singleton* Singleton::Holder::instance = new Singleton();
Singleton* Singleton::getInstance(){
	return Holder::instance;
}
int main() {
	Singleton* s1 = Singleton::getInstance();
	Singleton* s2 = Singleton::getInstance();

	if (s1 == s2)
		cout << "true" << endl;
}

编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton。第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有被任何线程锁定,因此其性能不会造成任何影响。通过使用IoDH,既可以实现延迟加载,又可以保证线程安全,不影响系统性能。因此,IoDH不失为一种最好的Java语言单例模式实现方式;其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH。

单例模式总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
1.主要优点
单例模式的主要优点如下:
(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3)允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称之为多例类。)
2.主要缺点
单例模式的主要缺点如下:
(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
(3)现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
在以下情况下可以考虑使用单例模式:
(1)系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2)客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值