揭秘单例模式:解读设计模式的奥秘

一、设计模式学习步骤

(1)设计模式解决什么问题?本质上是分析稳定点和变化点。在开发过程中要抽象稳定的,扩展变化点。设计模式具体解决问题的场景:希望修改少量的代码,就可以适应需求的变化。
(2)设计模式的代码结构是什么?代码结构反映出使用了什么设计模式。
(3)设计模式符合哪些设计原则?因为设计模式是由设计原则推导来的,可以按照设计模式产生的流程重新思考,能够很好的帮助我们去设计代码。
(4)如何在上面扩展代码?
(5)该设计模式有哪些典型应用场景。

学习步骤
设计模式解决什么问题
稳定点
变化点
设计模式的代码结构是什么
设计模式符合哪些设计原则
如何在上面扩展代码
该设计模式有哪些典型应用场景
联系工作场景
开源框架

二、C++的前置知识

class T{
public:
	T(){
		cout<<"T():"<<this<<endl;
	}
	~T(){
		cout<<"~T():"<<this<<endl;
	}
	T(const T&){
		cout<<"T(const T&) 拷贝构造:"<<this<<endl;
	}
	T& operator=(const T&){
		cout<<"T& operator=(const T&)拷贝赋值构造: "<<this<<endl;
	}
	T(T &&){
		cout<<"T(T &&)移动构造: "<<this<<endl;
	}
	T& operator=(T &&){
		cout<<"T& operator=(T &&)移动赋值构造: "<<this<<endl;
	}
};

T CreateT(){
	T temp;
	return temp;
}

2.1、拷贝构造

拷贝构造的触发有三个方式:
(1)直接用对象构造

T t1;
T t2=t1;

(2)传入参数构造

T t1;
T t2(t1);

(3)C++ 11出现的 初始化列表的构造方式

T t1;
T t2{t1};

2.2、拷贝赋值构造

两个对象之间赋值。

T t1;
T t2;
t1=t2;

2.3、移动构造

C++11出现的,有两种方式:
(1)函数l返回。

T t=CreateT();

注意,如果没有禁掉返回值优化:-fno-elide-constructors;就会只有一个构造函数和一个析构函数,只进行一次构造和一次析构。
如果禁掉返回值优化,编译器有三种行为:看类有没有移动构造;如果没有移动构造就看类有没有拷贝构造;如果两个都没有就会报错。

yes
no
yes
no
函数返回方式
禁掉返回值优化
不禁掉返回值优化
有没有移动构造函数
OK
有没有拷贝构造函数
error
只有一个构造函数
只有一个析构函数

(2)std::move()

T t1;
T t2(std::move(t1));

2.4、移动赋值构造

方式一:

T t;
t=T();

方式二:

T t1,t2;
t1=std::move(t2);

三、单例模式

在这里插入图片描述

3.1、定义

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

Singleton
- _instance
+Instance()

3.2、解决了什么问题

(1)稳定点:类只有一个实例,提供一个全局的访问点。抽象稳定点。
(2)变化点:有多个类都是单例,能不能复用代码。扩展变化点,继承、组合的方式。

3.3、代码结构

(1)私有的构造和析构。单例模式和程序的生命周期是相同的,不希望new和delete的存在,应用程序退出时单例模式才会释放。所以,需要把构造函数和析构函数隐藏起来,让用户不能调用。
(2)禁掉一些构造。把所有能构造的方式都关闭。比如 拷贝构造、拷贝赋值构造、移动构造、移动拷贝构造。
(3)静态类成员函数。
(4)静态私有成员变量。

3.4、示例一

class Singleton {
public:
    static Singleton * GetInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
       }
        return _instance;
   }
private:
    Singleton(){}; //构造
    ~Singleton(){};
    // = delete 就是关闭这些行为
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) =delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) =delete;//移动拷贝构造
    static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

_instance是静态全局分配的,程序退出自动释放。但是,_instance是一个指针,指向一个堆,_instance虽然释放了,但堆不自动释放。如果这个类是对文件操作的,那么程序退出时就无法关闭文件句柄和将未来得及写入文件的内容写入文件中。
当然,将_instance改用智能指针unique_ptr可以解决这个问题。

3.5、示例二

为解决上述的问题,增加一个接口。

class Singleton {
public:
    static Singleton * GetInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
            atexit(Destructor);// 当程序退出时调用atexit里设置的Destructor函数
       }
        return _instance;
   }
private:
    static void Destructor() {
        if (nullptr != _instance) { //
            delete _instance;
            _instance = nullptr;
       }
   }
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) =
delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) =
delete;//移动拷贝构造
    static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
// 还可以使⽤ 内部类,智能指针来解决; 此时还有线程安全问题

主要解决内存泄漏问题。

3.6、示例三

解决多线程的线程安全问题。

#include <mutex>
class Singleton { // 懒汉模式 lazy load
public:
    static Singleton * GetInstance() {
        // std::lock_guard<std::mutex>
lock(_mutex); // 3.1 切换线程
        if (_instance == nullptr) {
            std::lock_guard<std::mutex>
lock(_mutex); // 3.2
            if (_instance == nullptr) {
                _instance = new Singleton();
                // 1. 分配内存
                // 2. 调用构造函数
                // 3. 返回指针
                // 多线程环境下 cpu reorder操作
                atexit(Destructor);
           }
       }
        return _instance;
   }
private:
    static void Destructor() {
        if (nullptr != _instance) {
            delete _instance;
            _instance = nullptr;
       }
   }
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) =delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) =delete;//移动拷贝构造
    static Singleton * _instance;
    static std::mutex _mutex;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化

通过双重检测(double check)提升效率。
虽然加了锁,但是没有考虑到CPU的指令重排。new操作符在汇编中是有多个指令构成的,它会做这些工作:(1)分配内存、(2)调用构造函数、(3)返回指针;在多核环境下,CPU会进行指令重排(即不是按照我们规定的顺序1、2、3进行),这可能会造成程序奔溃。而这里的加锁,是从单线程的语义来的。

3.7、示例四

C++ 98是单线程语义,在多核时代,C++11在C++98的基础上做了一些优化(mutex、atomic、内存栅栏),编译器重排、CPU重排会违反顺序一致性。会产生可见性问题和执行序问题。
为解决这些问题,C++ 11提供了同步原语:原子变量和内存栅栏。

#include <mutex>
#include <atomic>

class Singleton{
public:
	static Singleton *GetInstance(){
		Singleton *tmp=_instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
		if(tmp==nullptr)
		{
			std::lock_guard<std::mutex> lock(_mutex);
			tmp=_instance.load(std::memory_order_relaxed);
			if(tmp==nullptr){
				tmp= new Singleton;
			
				std::atomic_thread_fence(std::memory_order_release);// 释放内存屏障
				_instance.store(tmp,std::memory_order_relaxed);
				atexit(Destructor);
			}
		}
		return tmp;
	}
private:
	static void Destructor(){
		Singleton* tmp=_instance.load(stdd::memory_order_relaxed);
		if(nullptr!=tmp)
			delete tmp;
	}
	Singleton(){};
	~Singleton(){};
	Singleton(const Singleton &) = delete;
	Singleton& operator=(const Singleton &) = delete;
	Singleton(Singleton &&)=delete;
	Singleton& operator=(Singleton &&)=delete;
	
	static std::atomic<Singleton*> _instance;
	static std::metex _mutex;
};

std::atomic<Singleton*> Singleton::_instance;//静态成员变量需要初始化
std::mutex Singleton::_mutex;//互斥锁初始化
// 编译
// g++ Singleton.cpp -o singleton -std=c++11

使用原子变量,原子变量解决了三个问题:
(1)原子执行的问题。
(2)可见性问题。使用load(可以看见其他线程最新操作的数据)和和store(修改数据让其他线程可见)来解决。
(3)执行绪问题。使用内存模型解决,memory_order_acquire、memory_order_release

3.8、示例五

上述代码略显赋值,所以可以做一下优化。c++11 magic static 特性:如果当变量在初始化的时候,并发同时进⼊声明语句,并发线程将会阻塞等待初始化结束。

// c++ effective
class Singleton
{
public:
    static Singleton& GetInstance() {
        static Singleton instance;
        return instance;
   }
private:
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) =delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) =delete;//移动拷贝构造
};
// 继承 Singleton
// g++ Singleton.cpp -o singleton -std=c++11

该版本具备 版本4 所有优点:

  1. 利⽤静态局部变量特性,延迟加载;
  2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函数;
  3. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;4. c++11 静态局部变量初始化时,具备线程安全。

3.9、示例六

添加多态。通过friend class让子类能够访问父类的private。

template<typename T>
class Singleton {
public:
	static T& GetInstance() {
        static T instance; // 这⾥要初始化DesignPattern,需要调⽤DesignPattern 构造函数,同时会调⽤⽗类的构造函数。
        return instance;
   }
protected:
    virtual ~Singleton() {}
    Singleton() {} // protected修饰构造函数,才能让别⼈继承
private:
    Singleton(const Singleton &) = delete; //拷⻉
构造
    Singleton& operator=(const Singleton&) =delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) =delete;//移动拷贝构造
};

class DesignPattern : public Singleton<DesignPattern> {
	//friend 能让Singleton<T> 访问到 DesignPattern构造函数
    friend class Singleton<DesignPattern>; 
private:
    DesignPattern() {}
    ~DesignPattern() {}
};

解决变化点问题,如果项目中有多个类都要写单例模式,通过继承的方式进行扩展。使用模板和友缘类。

总结

通过六个示例描述一步步完善单例模式的设计过程,需要考虑的问题。
C++类的构造有:构造函数、拷贝构造、拷贝赋值构造、移动构造、移动赋值构造。
编写单例模式代码时,需要考虑其线程安全性问题。
同一个对象,它们是friend class的关系,可以互相访问私有成员。单例模式是很常见的设计模式,需要掌握。

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lion Long

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

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

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

打赏作者

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

抵扣说明:

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

余额充值