C++ 设计模式之单例模式

“天上地下,唯我独尊” —— 单例模式

前言

你能在电脑上调出两个 WIndows 任务管理器吗?

假设能,如果两个管理器显示的数据相同,那又何必存在两个呢?
如果两个管理器显示的数据不同,那我应该相信哪一个呢?

试试看,应该有且仅有一个吧?一个系统里有且仅有一个 Windows 任务管理器实例供外界访问。如何保证系统里有且仅有一个实例对象呢?并且能够供外界访问?你可以在系统里定义一个统一的全局变量,但这并不能防止创建多个多个对象(想一想,为什么?)这就是单例模式的典型应用。

对于一个软件系统中的某些类来说,只有一个实例很重要。假设 Windows 系统上可以同时调出两个 Windows 任务管理器,这两个任务管理器显示的都是同样的信息,那势必会造成内存资源的浪费;如果两个任务管理器显示的是不同的信息,这也给用户带来了困惑,到底哪一个才是真实的状态?


1. 单例模式简介

单例模式
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

单例模式有3个要点:

  1. 这个类只有一个实例
  2. 它必须自己创建实例
  3. 它必须自己向整个系统提供这个实例

2. 单例模式结构

单例模式结构非常简单,其 UML 图如下所示,只包含一个类,即单例类。为防止创建索格对象,其构造函数必须是私有的(外界不能访问)。另一方面,为了提供一个全局访问点来访问该唯一实例,单例类提供了一个公有方法 getInstance 来返回该实例。
在这里插入图片描述


3. 单例模式代码及效果

【注意:下列代码均在 Linux 环境下进行测试】

3.1 单例模式代码及验证

单例模式代码:

#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton{
private:
    static Singleton *m_instance;
    // 构造函数私有
    Singleton(){}
    // 拷贝构造和赋值运算符私有
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton&) = delete;
public:
    static Singleton *getInstance()
    {
            if(nullptr == m_instance)
            {
                    cout << "创建新的实例" << endl;
                    m_instance = new Singleton();
            }
            return m_instance;
    }
};
Singleton* Singleton::m_instance = nullptr;
#endif // SINGLETON_H

可以看到,构造函数是私有的,拷贝构造函数和赋值运算符也不能使用,即单例模式对象只能在类内部实例化,这就满足了单例模式的第二个要点(即自己创建实例对象)。同时,实例对象 m_instance 是静态的,也就是全局的。假设客户端实例化了两个 Singleton,但是 m_instance 只有一个(这就满足了单例模式只有一个实例对象)。那第三个要点怎样满足呢?即外界如何获取单例对象?上述代码中定义了一个方法 getInstance() 便是获取单例对象。

下面看看客户端怎样使用?
客户端验证代码:

#include <iostream>
#include "Singleton.h"
using namespace std;
void test1()
{
    Singleton *s1 = Singleton::getInstance();
    Singleton *s2 = Singleton::getInstance();
}
int main()
{
    test1();
    return 0;
}

效果如下图:虽然客户端创建了2次实例对象,但是实际上实例只创建了一次。
在这里插入图片描述
上述的客户端验证似乎说明上述代码实现了单例模式。的确是实现了,但是这样真的安全吗?试想在多线程环境里,当两个线程(甚至更多线程)同时使用,同样存在创建了多个实例的隐患。这便引入了多线程环境的单例模式。


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

我在Linux环境下模拟了10个线程同时使用该对象,那么会像预期的那样只创建一个实例吗?
客户端测试代码:

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

static int num = 1;
void *GetInstance(void *arg)
{
	Singleton *s1 = Singleton::getInstance();
	sleep(5);
	printf("线程编号为%d\n", num++);
	return NULL;
}
void test2()
{
	pthread_t tid[10];
	for(int i = 0; i < 10; i++)
	{
		pthread_create(&tid[i], NULL, GetInstance, NULL);
	}
	for(int i = 0; i < 10; i++)
	{
		pthread_join(tid[i], NULL);
	}
}
int main()
{
	test2();
	return 0;
}

一共创建了10个线程,每个线程里面都试图创建一个单例对象。理论上,最终只有第一个线程(死一个被系统调度的线程)才能打印出 “创建新的实例”,然后,运行结果如下:
在这里插入图片描述
上述结果不言而喻了,3.1 的单例模式的代码并不是线程安全的。


3.3 线程安全的单例模式的代码实现

如何做到线程安全呢?多线程同步与互斥有多重方法,这里主要介绍互斥量这种方法。

代码如下:

#include <iostream>
#include <pthread.h>
using namespace std;
class Singleton{
private:
    static Singleton *m_instance;
    static pthread_mutex_t mutex;
    // 构造函数私有
    Singleton(){}
    // 拷贝构造和赋值运算符私有
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton&) = delete;
public:
    static Singleton *getInstance()
    {
            if(nullptr == m_instance)
            {
                    // 加锁
                    pthread_mutex_lock(&mutex);
                    if(nullptr == m_instance)
                    {
                        cout << "创建新的实例" << endl;
                        m_instance = new Singleton();
			pthread_mutex_lock(&mutex);
                    }   
            }
            return m_instance;
    }
};
Singleton* Singleton::m_instance = nullptr;
pthread_mutex_t Singleton::mutex;

客户端代码不变,运行结果如下
在这里插入图片描述
加了锁机制后,实现了在多线程的环境下也只会创建一个实例。

但是,你会不会对下列代码有一个疑问?为什么需要判断两次 m_instance 是否是空?
在这里插入图片描述

第一次判断:是判断当前是否创建了实例对象,如果没有创建,则先加锁再创建。
第二次判断:是在加锁之后再做一次判断。你是否会觉得这部操作很多余。不,这步必须有,因为有这么一种情况,就是在加锁的过程中,有别的线程刚好就在加锁的时间内创建出了这个实例对象,所以为了避免出现这种情况出现,还需要再判断一次。


4. 单例模式总结

优点:

  • 单例模式提供了严格的对唯一实例的创建和访问
  • 单例模式的实现可以节省系统资源
    缺点
  • 如果某个实例负责多重职责但又必须实例唯一,那单例类的职责过多,这违背了单一职责原则
  • 多线程下要考虑线程安全机制
  • 单例模式没有抽象层,不方便扩展
    适用环境:
  • 系统只需要一个实例对象
  • 某个实例允许有一个访问接口
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值