掘金 阅读 ( ) • 2024-04-11 18:03

意图

将工作中的零星知识点串起来。 单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  • 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。 如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

单例模式适合应用场景

  1. 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
  • 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
  1. 如果你需要更加严格地控制全局变量, 可以使用单例模式。
  • 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。

代码示例

非线程安全

基础单例

#pragma once
#include <iostream>
using namespace std;

class Singleton
{
protected:
	//构造函数和析构函数不共有,禁止外部调用new delete方法创建和销毁对象
	Singleton(const std::string value) : value_(value)
	{
		std::cout << "Singleton Construct  " << std::endl;
	}

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

	static Singleton* singleton_;

	std::string value_;

public:
	//禁止拷贝构造和拷贝赋值
	Singleton(Singleton& other) = delete;
	void operator=(const Singleton&) = delete;
	
	//静态方法获取单例,通过类名即可访问
	static Singleton* GetInstance(const std::string& value);
	
	//可以添加单例类逻辑处理
	void SomeBusinessLogic()
	{
		// ...
	}

	std::string value() const {
		return value_;
	}
};

//静态变量初始化
Singleton* Singleton::singleton_ = nullptr;;

Singleton* Singleton::GetInstance(const std::string& value)
{
	//多线程调用时,new 单例对象不安全
	if (singleton_ == nullptr) {          // 1
		singleton_ = new Singleton(value);
	}
	return singleton_;
}
#include "Singleton.h"
#include <thread>

void ThreadFoo() {
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("FOO");
	std::cout << singleton->value() << "\n";
}

void ThreadBar() {
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("BAR");
	std::cout << singleton->value() << "\n";
}


int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

输出结果: Singleton Construct Singleton Construct

BAR FOO

此单例非线程安全,因为代码中1处操作不安全,可以把new 操作用时拉长便可理解,线程 t1 判断单例句柄为空便执行new操作,在new操作执行中时,线程 t2 判断单例句柄为空,便又执行了一遍new操作。

线程安全

饿汉式

#pragma once
#include <iostream>
using namespace std;

//饿汉式
class Singleton
{
private:
	Singleton(const std::string value) : value_(value)
	{
		std::cout << "Singleton Construct  " << std::endl;
	}

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

	static Singleton* singleton_;

	std::string value_;

public:

	Singleton(Singleton& other) = delete;
	void operator=(const Singleton&) = delete;

	static Singleton* GetInstance(const std::string& value)
	{
		if (singleton_ == nullptr) {
			singleton_ = new Singleton(value);
		}
		return singleton_;
	}

	void SomeBusinessLogic()
	{
		// ...
	}

	std::string value() const {
		return value_;
	}
};
#include "Singleton.h"
#include <thread>

Singleton* Singleton::singleton_ = Singleton::GetInstance("first");  //关键点!!!

void ThreadFoo() {
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("FOO");
	std::cout << singleton->value() << "\n";
}

void ThreadBar() {
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("BAR");
	std::cout << singleton->value() << "\n";
}


int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

输出结果: Singleton Construct first first

非线程安全版摇身一变成为了线程安全版本,关键点在于静态实例句柄初始化放在了主线程进行,后面t1 t2线程再调用就ok了。

优点

  1. 在性能需求较高时,应使用这种模式,避免频繁的锁争夺。这个优点可以通过局部静态变量来实现,往下看。

以下缺点

  1. 不论是否需要使用该对象都将其定义出来,可能浪费了内存,或者减慢了程序的启动速度
  2. 饿汉式是从使用角度规避多线程的安全问题,但很难从规则角度限制开发人员,所以这种方式不是很推荐。

懒汉式

针对上述饿汉版缺点,调用初始化不应该加以限制,所以就有了懒汉式方式初始化资源,在用到时如果没有初始化单例则初始化,如果初始化了则直接使用. 所以这种方式我们要加锁,防止资源被重复初始化。

基础版-DoubleCheck

#pragma once
#include <string>
#include <mutex>
#include <iostream>

class Singleton
{

private:
	//静态成员变量  类共有
	static Singleton* pinstance_;
	static std::mutex mutex_;

	//禁止外部new delete操作
	Singleton(const std::string value) : value_(value) {
		std::cout << "Singleton" << std::endl;
	}
	~Singleton() {}

	std::string value_;

public:
	//禁止外部拷贝
	Singleton(Singleton& other) = delete;
	void operator=(const Singleton&) = delete;

	//线程安全单例
	static Singleton* GetInstance(const std::string& value)
	{
		if (pinstance_ != nullptr)    //1 读变量不加锁
		{
			return pinstance_;
		}
		mutex_.lock();
		if (pinstance_ != nullptr)
		{
			mutex_.unlock();
			return pinstance_;
		}
		pinstance_ = new Singleton(value);   //2 变量操作加锁
		mutex_.unlock();
		return pinstance_;
	}

	//单例逻辑
	void SomeBusinessLogic()
	{
		// ...
	}

	std::string value() const {
		return value_;
	}
};

调用方式

#include "Singleton.h"


//pinstance_初始化为空  mutex_调用无参构造初始化
Singleton* Singleton::pinstance_{ nullptr };
std::mutex Singleton::mutex_;

void ThreadFoo() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("FOO");
	std::cout << singleton->value() << "\n";
}

void ThreadBar() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	Singleton* singleton = Singleton::GetInstance("BAR");
	std::cout << singleton->value() << "\n";
}

int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

执行结果: Singleton BAR BAR

使用Double-Checked Locking Pattern (DCLP)。使用两次判断来解决线程安全问题并且提高效率。

问题 1

此版本是线程不安全的,原因如下:

new Singleton(value) 实际上做了三件事,第一件事申请一块内存,第二件事调用构造函数,第三件是将该内存地址赋给pinstance_ 。

不同的编译器表现是不一样的。可能存在的情况是先将该内存地址赋给pinstance_ ,然后再调用构造函数。这是线程 t1 恰好申请完成内存,并且将内存地址赋给pinstance_ ,但是还没调用构造函数的时候。线程 t2 执行到语句1处,判断pinstance_ 此时不为空,则返回该变量,然后调用该对象的函数,但是该对象还没有进行构造。

问题 2

当多个线程都调用单例函数时,我们不确定资源是被哪个线程初始化的。 回收指针存在问题,存在多重释放或者不知道哪个指针释放的问题。

改良版本

针对上述问题提出改良版,使用智能指针解决资源释放问题。

#pragma once
#include <string>
#include <mutex>
#include <iostream>

class Singleton;

class AutoDeletor
{
public:
	void operator()(Singleton* sf)
	{
		std::cout << "this is safe deleter operator()" << std::endl;
		delete sf;
	}
};

class Singleton
{

private:
	//静态成员变量  类共有
	static std::shared_ptr<Singleton> pinstance_;
	static std::mutex mutex_;

	//禁止外部new delete操作
	Singleton(const std::string value) : value_(value) {
		std::cout << "Singleton" << std::endl;
	}
	~Singleton() {}

	std::string value_;

	//定义友元类,通过友元类调用该类析构函数
	friend class AutoDeletor;
public:

	//禁止外部拷贝
	Singleton(Singleton& other) = delete;
	void operator=(const Singleton&) = delete;

	//线程安全单例
	static std::shared_ptr<Singleton> GetInstance(const std::string& value)
	{
		std::lock_guard<std::mutex> lock(mutex_);
		if (pinstance_ == nullptr)
		{
			pinstance_ = std::shared_ptr<Singleton>(new Singleton(value), AutoDeletor());;
		}
		return pinstance_;
	}

	//单例逻辑
	void SomeBusinessLogic()
	{
		// ...
	}

	std::string value() const {
		return value_;
	}
};

调用方式

#include "Singleton.h"


//pinstance_初始化为空  mutex_调用无参构造初始化
std::shared_ptr<Singleton> Singleton::pinstance_{ nullptr };
std::mutex Singleton::mutex_;

void ThreadFoo() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::shared_ptr<Singleton> singleton = Singleton::GetInstance("FOO");
	std::cout << singleton->value() << "\n";
}

void ThreadBar() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::shared_ptr<Singleton> singleton = Singleton::GetInstance("BAR");
	std::cout << singleton->value() << "\n";
}

int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

执行结果: Singleton FOO FOO this is safe deleter operator()

这个版本解决double check问题,使用智能指针自动释放资源。

升级版 call_once

c++11后保证多线程中局部静态变量也只会初始化一次(这句话很重要,后面的局部静态变量版单例就是依据这个实现),故可使用 std::call_once 配合 std::once_flag 使用以实现线程安全的初始化。 多线程调用call_once函数时,会判断once_flag是否被初始化,如没被初始化则进入初始化流程,调用我们提供的初始化函数。 但是同一时刻只有一个线程能进入这个初始化函数。

#pragma once
#include <memory>
#include<mutex>
#include<iostream>

class Singleton;

class AutoDeletor
{
public:
	void operator()(Singleton* sf)
	{
		std::cout << "this is safe deleter operator()" << std::endl;
		delete sf;
	}
};

class Singleton {
private:
	//禁止外部调用new delete操作对象
	Singleton(const std::string value) : value_(value) {
		std::cout << "Singleton" << std::endl;
	}

	~Singleton() {
		std::cout << "singleton destruct" << std::endl;
	}

	Singleton(const Singleton&) = delete;
	void operator = (const Singleton& st) = delete;

	static std::shared_ptr<Singleton> _instance;
	std::string value_;

	//定义友元类,通过友元类调用该类析构函数
	friend class AutoDeletor;
public:
	static std::shared_ptr<Singleton> GetInstance(const std::string& value) {
		static std::once_flag s_flag;
		std::call_once(s_flag, [&]() {
			_instance = std::shared_ptr<Singleton>(new Singleton(value), AutoDeletor());;
			});

		return _instance;
	}

	std::string value() const {
		return value_;
	}
};

std::shared_ptr<Singleton> Singleton::_instance = nullptr;

调用方式

#include "Singleton.h"

void ThreadFoo() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::shared_ptr<Singleton> singleton = Singleton::GetInstance("FOO");
	std::cout << singleton->value() << "\n";
}

void ThreadBar() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::shared_ptr<Singleton> singleton = Singleton::GetInstance("BAR");
	std::cout << singleton->value() << "\n";
}

int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

输出结果: Singleton FOO FOO this is safe deleter operator() 单例已经生效

推介版-局部静态变量

《Effective C++》中提出了一种简洁的singleton写法: 原理: 在C++11之前的标准中并没有规定local static变量的内存模型。于是乎它就是不是线程安全的了。但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它。 优点:

  1. 不需要使用共享指针,代码简洁;
#pragma once
#include<iostream>

class Singleton {
private:
	Singleton(const std::string value) : value_(value) {
		std::cout << "Singleton Construct" << std::endl;
	}

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

	std::string value_;

	Singleton(const Singleton&) = delete;
	void operator=(const Singleton&) = delete;

public:
	static Singleton& GetInstance(const std::string& value)
	{
		static Singleton single(value);
		return single;
	}

	std::string value() const {
		return value_;
	}
};

调用方式如下

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

void ThreadFoo() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << Singleton::GetInstance("FOO").value() << "\n";
}

void ThreadBar() {
	// Following code emulates slow initialization.
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << Singleton::GetInstance("BAR").value() << "\n";
}

int main()
{
	std::thread t1(ThreadFoo);
	std::thread t2(ThreadBar);
	t1.join();
	t2.join();

	return 0;
}

输出结果: Singleton Construct FOOFOO

Singleton Destruct

注意:所以为了保证运行安全请确保使用C++11以上的标准!!!

通用版

为了使用单例类更通用,比如项目中使用多个单例类,可以通过继承实现多个单例类

template<typename T>
class Singleton
{
public:
	static T& GetInstance()
	{
		static T instance;
		return instance;
	}

	Singleton(T&&) = delete;
	Singleton(const T&) = delete;
	void operator= (const T&) = delete;

protected:
	Singleton() = default;
	virtual ~Singleton() = default;
};

通过禁用单例类的 copy constructor,move constructor 和 operator= 可以防止类的唯一实例被拷贝或移动;不暴露单例类的 constructor 和 destructor 可以保证单例类不会通过其他途径被实例化,同时将两者定义为 protected 可以让其被子类继承并使用。

调用方式:

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

//想使用单例类,可以继承上面的模板
class Foo : public Singleton<Foo>
{
public:
	void operator() ()
	{
		std::cout << &GetInstance() << "   " << std::endl;
	}
};

int main()
{
	std::thread t1((Foo()));
	std::thread t2((Foo()));
	t1.join();
	t2.join();
	std::this_thread::sleep_for(std::chrono::milliseconds(100));

	return 0;
}

输出结果: 00D32000 00D32000

源码链接:https://gitee.com/qinxiaowen/design_patterns/tree/master/design_patterns

参考链接: https://llfc.club/ https://zhuanlan.zhihu.com/p/232319083

相关内容