可以扩展的多线程单例模式
单例模式真可谓非常出名了, 但是一般大家的探究仅仅止步于如何从多线程安全的角度来实现它.
我今天在网友的智慧的基础上往前再走一步, 提出可以支持扩展的单例模式.
在这篇文章中, 你将了解到4中单例模式, 它们层层递进. 它们分别是
(1) 隐藏构造函数的基本单例模式
(2) 支持多线程的单例模式
(3) 使用模板支持扩展的借助 static 成员属性的单例模式
(4) 使用模板支持扩展的借助 static 局部变量的单例模式
最后两个方法是我本人想了三个小时摸索出来的, 我还没有看到其他人有这么写过,所以保证是原创, 向毛主席保证.
下面我将逐步实现它们, 并且把代码展示出来, 在文章的末尾有代码的Github 链接.
(1) 隐藏构造函数的基本单例模式
// Created by zxzx on 2021/1/13.
//
class Singleton{
private:
Singleton(){};
static Singleton *instance;
public:
static getInstance(){
if(instance== nullptr){
instance = new Singleton();
}
return instance;
};
};
Singleton* Singleton::instance = nullptr ;
这种就不用解释了, 经典中的经典.
(2) 支持多线程的单例模式
//
// Created by zxzx on 2021/1/13.
//
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
class Singleton{
private:
Singleton(){ // 这个方法导致Singleton无法被继承
cout << "call constructor" << endl;
};
static Singleton *instance;
static mutex m_mutex;
public:
static Singleton* getInstance(){
if(instance== nullptr){
lock_guard<mutex> guard(m_mutex); // 使用lock_guard, 防止锁无法释放
if(!instance){
instance = new Singleton(); // 这里有一个问题:要不要把T的构造函数设置为私有的呢? 然后把 Singleton 类做为其友元?这里可以考虑如此设计。
}
}
return instance;
};
static void delInstance(){
if(instance!= nullptr){
lock_guard<mutex> guard(m_mutex); // 使用lock_guard, 防止被多次释放
if(instance){
delete instance;
instance = nullptr;
cout << "deconstruct instance" << endl;
}
}
}
};
Singleton* Singleton::instance = nullptr ;
mutex Singleton::m_mutex;
void f0(){
Singleton* single = Singleton::getInstance();
cout << single << endl; // 打印地址
}
void tets_multithread(){
thread t1(f0);
t1.join();
thread t2(f0);
t2.join();
thread t3(f0);
t3.join();
thread t4(f0);
t4.join();
thread t5(f0);
t5.join();
/** 打印结果如下:
*
call constructor
0x7f8fc14029e0
0x7f8fc14029e0
0x7f8fc14029e0
0x7f8fc14029e0
0x7f8fc14029e0
*
* */
}
void f2(){
Singleton* single = Singleton::getInstance();
Singleton* single2 = Singleton::getInstance();
cout << single << endl;
cout << single2 << endl;
Singleton::delInstance();
single = Singleton::getInstance();
cout << single << endl;
}
int main(){
f2();
// tets_multithread();
}
在这个例子中, 我们使用了 c++11的 mutex 类以及 lock_guard类, 可以保证 mutex 被及时的释放掉.
lock_guard 是一个位于 头文件中的类, 定义在 std namespace 中.
我们分别启动了五个线程测试,可以看到, 实例始终是一个对象,因为他们的地址相同.
(3) 使用模板支持扩展的借助 static 成员属性的单例模式
像上面的单例模式, 已经很接近完美了(忽略返回的是对象 还是指针, 忽略转移语义,拷贝函数等等C++语言细节)
但是有一个致命的问题就是, 它无法扩展, 如果我需要一个单例类,我需要从0开始设计, 每次都要写相同的代码,那么就非常的愚蠢了.
那么我们应该如何设计扩展可扩展的单例模式呢?
假设我的目标是这样子的, 我有一个类 A, 它是普通类, 然后我把它传入一个模板类 Singleton<T>
里, 那么我们就拥有了一个单例类, Singleton<A>
, 这样是不是非常的巧妙!!
所以基于刚才那个问题, 我考虑过几个问题:
(1)使用继承, 只要继承了这个单例类, 我们自己的类就是一个单例类, 这样听起来很棒, 也许可以尝试者实现一下, 我试过, 但是当时不知道怎么的卡主了, 就没往下走了.
我遇到的一个问题是: 因为我们把构造函数私有了之后, 这个类就没有办法被继承了, 所以继承这条路暂时走不通.
(2)使用模板, 或者说泛型编程(Java 方言) .
我的初步设计是这样的, 我们先写一个普通的类T(它不是单例模式的类, 可以自由的创建实例),然后把这个类传到模板类 TMP 里面去, 然后让这个模板类TMP返回一个 T类型的指针.
具体实现把我们自己的类 T 作为一个static 的成员属性,这样就支持扩展了.
这里有个难题是, 传入的类能当做静态属性吗? 后来我找了网上的例子,发现确实可以, 尝试了好久, 才最终写出这样的代码.
我的第一版代码长这样
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
template <typename T>
class Singleton{
private:
static mutex m_mutex;
Singleton(){};
Singleton(Singleton& other);
Singleton& operator=(Singleton& other);
static T * instance;
public:
static T* getInstance(){ //这里应该返回
if(!instance){
lock_guard<mutex> guard(m_mutex); // 使用lock_guard, 防止锁无法释放
if(!instance){
instance = new T(); // 这里有一个问题:要不要把T的构造函数设置为私有的呢? 然后把 Singleton 类做为其友元?这里可以考虑如此设计。
}
}
return instance;
};
};
template <typename T>
mutex Singleton<T>::m_mutex; //
template <typename T>
T* Singleton<T>::instance;
测了了一下
class A{
public:
int i1 = 1;
void f1(){
std::cout << "class A instance: " << this << std::endl;
}
};
class B{
public:
int i1 = 1;
void f1(){
std::cout << "class B instance: " << this << std::endl;
}
};
void f0(){
A* single = Singleton<A>::getInstance();
single->f1();
A* single2 = Singleton<A>::getInstance();
single2->f1();
A* a = new A();
a->f1();
B* single3 = Singleton<B>::getInstance();
single3->f1();
}
int main(){
// f2();
f0();
}
// class A instance: 0x7fc27fc02990
// class A instance: 0x7fc27fc02990
// class A instance: 0x7fc27fc029a0 // 自己也可以新建类 ,后缀 a0 != 90
// class B instance: 0x7fc27fc029b0
这种方法是有问题的, 有什么问题呢?
(1) 返回我们自己传入的类的指针, 那么万一被用户错误的释放了怎么办? 比如 delete.
那么如果这个类自己调用 new ,返回的指针类型和这个单例模式返回的类岂不是一样的了!!
所以, 我们要返回一个 Singleton 类型的指针. 我们对代码做如下修改
//
// Created by zxzx on 2021/1/13.
//
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
template <typename T>
class Singleton{
private:
static mutex m_mutex;
Singleton(){};
Singleton(Singleton& other);
Singleton& operator=(Singleton& other);
public:
T t; // 我们自己的类, 嵌入在这个Singleton类中, 这里需要考虑的是构造传参问题, 可以考虑设计成指针, 从而更好地控制构造参数的调用
static Singleton* instance;
static Singleton* getInstance(){ //这里应该返回
if(!instance){
lock_guard<mutex> guard(m_mutex); // 使用lock_guard, 防止锁无法释放
if(!instance){
instance = new Singleton();
}
}
return instance;
};
};
template <typename T>
mutex Singleton<T>::m_mutex; // 泛型是也是可以使用静态成员的,只不过在外面初始化属性的时候要带上泛型定义
template <typename T>
Singleton<T>* Singleton<T>::instance;
测试一下,
class A{
public:
int i1 = 1;
void f1(){
std::cout << "class A instance: " << this << std::endl;
}
};
class B{
public:
int i1 = 1;
void f1(){
std::cout << "class B instance: " << this << std::endl;
}
};
void f0(){
Singleton<A>* single = Singleton<A>::getInstance();
single->t.f1();
Singleton<A>* single2 = Singleton<A>::getInstance();
single2->t.f1();
Singleton<B>* singleb = Singleton<B>::getInstance();
singleb->t.f1();
Singleton<B>* singleb2 = Singleton<B>::getInstance();
singleb2->t.f1();
}
int main(){
f0();
}
/*
class A instance: 0x7ffc18402990
class A instance: 0x7ffc18402990
class B instance: 0x7ffc184029a0
class B instance: 0x7ffc184029a0
*
* */
这里有一个问题:要不要把T的构造函数设置为私有的呢? 然后把 Singleton 类做为其友元?这里可以考虑设计一下,
因为我们毕竟设计了一个类, 我们只想把它当做单例模式用, 不希望它可以被自由的创建实例, 所以我们可以写成这样
class A{
private:
~A(){} // 代表各种构造函数, 包括拷贝构造
friend Singleton; // 把单例模式作为其you元类
public:
int i1 = 1;
void f1(){
std::cout << "class A instance: " << this << std::endl;
}
};
(4) 使用模板支持扩展的借助 static 局部变量的单例模式
这里用个小trick, 只适合C/C++, 因为他们有 static 临时变量(好骚的语法)
//
// Created by zxzx on 2021/1/13.
//
// 单线程版本的
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
template <typename T>
class Singleton{
private:
static mutex m_mutex;
Singleton(){};
Singleton(Singleton& other);
Singleton& operator=(Singleton& other);
public:
T t;
static Singleton* getInstance(){ //这里应该返回
static Singleton* instance;
if(!instance){
lock_guard<mutex> guard(m_mutex); // 使用lock_guard, 防止锁无法释放
if(!instance){
instance = new Singleton();
}
}
return instance;
};
};
template <typename T>
mutex Singleton<T>::m_mutex;
测试代码如下:
class A{
public:
int i1 = 1;
void f1(){
std::cout << "class A instance: " << this << std::endl;
}
};
class B{
public:
int i1 = 1;
void f1(){
std::cout << "class B instance: " << this << std::endl;
}
};
void f0(){
Singleton<A>* single = Singleton<A>::getInstance();
single->t.f1();
Singleton<A>* single2 = Singleton<A>::getInstance();
single2->t.f1();
Singleton<B>* singleb = Singleton<B>::getInstance();
singleb->t.f1();
Singleton<B>* singleb2 = Singleton<B>::getInstance();
singleb2->t.f1();
}
int main(){
// f2();
f0();
}
/*
class A instance: 0x7ffc18402990
class A instance: 0x7ffc18402990
class B instance: 0x7ffc184029a0
class B instance: 0x7ffc184029a0
*
* */
总结
我们实现了4种单例模式, 后面两种是可以扩展的. 其核心思想是, 我们写一个类A, 可以通过模板 Singleton 来实现一个自动的单例类 Singleton<A>
, 或者 Singleton<B>
, 而A和B依然可以当做普通类来用, 如果介意这样, 还可以把它们的构造方法隐藏起来, 把 Singleton 类作为其友元, 只能通过 Singleton<A/B> 来获得单例类.
具体的做法是, 先使用普通的单例模式, 然后把这种模式变成一个模板类, 给模板类里面增加一个我们的类的成员属性, 然后通过模板类本身的单例来获取我们的自己的类的单例. 单例是可以传播的, 原理就是这么简单.
希望对大家有帮助.
代码地址: https://github.com/notfresh/cpp_learn/tree/master/designPattern
参考
[1]单例模式 冯Jungle
https://blog.youkuaiyun.com/sinat_21107433/article/details/102649056
[2]C++/Java__中类模板中的静态成员变量和静态成员函数
https://blog.youkuaiyun.com/u010003835/article/details/49104989
[3]C++中的单例模式
https://blog.youkuaiyun.com/hackbuteer1/article/details/7460019