模式简介
- 一个类只允许有一个实例,并提供一个访问该实例的全局访问点。
模式结构
单例模式主要有两种类角色:
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
实现单例的两种模式
- 饿汉模式:程序加载时就创建一个单例
- 懒汉模式:第一次调用
GetInstance() 方法时才去创建这个单例
非线程安全单例
#include <thread>
#include <iostream>
using namespace std;
class Singleton {
protected:
string value;
static Singleton* instance;
Singleton(const string& v) : value(v) { }
public:
Singleton(Singleton& s) = delete;
void operator=(const Singleton& s) = delete;
static Singleton* GetInstance(const string& v) {
if (instance == nullptr) {
instance = new Singleton(v);
}
return instance;
}
void Print() {
cout << value << endl;
}
};
Singleton* Singleton::instance = nullptr;
void threadProcess(const string& s) {
Singleton* t = Singleton::GetInstance(s);
t->Print();
cout << t << endl;
}
int main() {
thread thd1 = thread(threadProcess, "bar");
thread thd2 = thread(threadProcess, "foo");
thd1.join();
thd2.join();
}
输出:
bar
bar
0000026D3FB3CC20
0000026D3FB3CC20
foobar
000001EC00BACE50
000001EC00BACD70
线程安全单例
在基础单例中,考虑到这样的一个场景:假设线程1先执行并且在未完成实例创建之前,线程2执行了条件判断,则线程2会创建一个新的实例(并且会造成内存泄漏)。
解决的方案是:在GetInstance() 函数中加入锁限制每次只能有一个线程调用,改进后的代码如下:
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;
class Singleton {
protected:
string value;
static mutex mtx;
static Singleton* instance;
Singleton(const string& v) : value(v) {
}
public:
Singleton(Singleton& s) = delete;
void operator=(const Singleton& s) = delete;
static Singleton* GetInstance(const string& v) {
unique_lock<mutex> lck(mtx);
if (instance == nullptr) {
instance = new Singleton(v);
}
return instance;
}
void Print() {
cout << value << endl;
}
};
mutex Singleton::mtx;
Singleton* Singleton::instance = nullptr;
void threadProcess(const string& s) {
Singleton* t = Singleton::GetInstance(s);
t->Print();
cout << t << endl;
}
int main() {
thread thd1 = thread(threadProcess, "bar");
thread thd2 = thread(threadProcess, "foo");
thd1.join();
thd2.join();
}
输出
barbar
0000024B5482CFA0
0000024B5482CFA0
示例代码(Git)
应用场景
- 某类只要求生成一个对象的时候。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 需要频繁使用的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 当对象可以被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
模式优缺点
优点
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
与其他模式的关系
- 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
- 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
参考:
- http://c.biancheng.net/view/1338.html
- https://refactoringguru.cn/design-patterns/singleton
- https://www.w3cschool.cn/shejimoshi/singleton-pattern.html
|