一、单例模式概念
单例模式(Singleton)是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例 (Singleton) 类声明了一个名为
getInstance
获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端 (Client) 代码隐藏。 调用
getInstance
方法必须是获取单例对象的唯一方式。所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。- 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
适用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
- 如果你需要更加严格地控制全局变量, 可以使用单例模式。 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
单例设计模式的结构:
单例 (Singleton) 类声明了一个名为
getInstance
获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端 (Client) 代码隐藏。 调用获取实例
方法必须是获取单例对象的唯一方式。
代码如下:
问题:对于一些类来说,只有一个实例是很重要的。例如数据库或其共享资源的访问权限。并且这个实例需要易于被访问。
解决方案:保证一个类只有一个实例,并提供一个访问它的全局访问点。
单例模式实现的三种方法:(具体实现看代码)
1.懒汉式:你不找懒汉,懒汉根本就懒得去初始化自己。
优点:
缺点:当有两个线程调
getInstance()
方法,当它们同时执行到if (null == instance)
这行代码,instance
为null
。2.饿汉式:饿汉根本等不及别人来找他,不管三七二十一先初始化了自身的实例,生怕自己饿着了。
优点:规避了懒汉式方法的线程问题,不用显示编写线程安全代码。
缺点:在没有必要获取实例时,已经预先产生了开销。
3.双重锁的形式:如果既不想在没有调用
getInstance(
) 方法时产生开销,又不想发生线程安全问题,就可以采用双重锁的形式。问题:在外面判断了 instance 实例是否存在,为什么在锁定后又要在内部又判断一次?
这是因为,如果
instance
为null
时有两个线程同时调用getInstance()
,由于synchronized
机制,只允许一个线程进入,另一个需要等待。这时如果没有第二道instance
是否为null
的判断,就可能发生第一个线程创建一个实例,而第二个线程又创建一个实例的情况。
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
class Singleton
{
private:
Singleton(const std::string value) : m_value(value)
{
}
~Singleton(){}
string m_value;
public:
Singleton(Singleton& other)=delete;
Singleton(const Singleton& other)=delete;
Singleton& operator=(const Singleton& other)=delete;
string value()const{return m_value;}
static Singleton* getInstance(const std::string& value);
private:
static Singleton* m_instance;
static mutex m_mutex;
};
//静态函数在类外初始化
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::m_mutex;
Singleton* Singleton::getInstance(const std::string& value)
{
//方法1:懒汉式:在单例对象第一次被使用时,才会创建实例。这种方式在单例对象未被使用时不会立即创建实例,这样可以节省资源。
/*lock_guard<mutex> lock(m_mutex);
if (m_instance == nullptr)
{
m_instance = new Singleton(value);
}
return m_instance;*/
//方法2:饿汉式:在程序启动时,单例对象就被创建并初始化。这种方式在单例对象被第一次使用时会立即创建实例,因此不需要考虑线程安全问题。
/*static Singleton* s = new Singleton(value);
return s;*/
//方法3:双重检查锁定:在多线程环境下,使用双重检查锁定来确保只有一个线程能够创建单例对象。这种方式在单例对象
//未被使用时不会立即创建实例,可以节省资源,同时也能保证线程安全。
if (m_instance == nullptr)
{
lock_guard<mutex> lock(m_mutex);
if (m_instance == nullptr)
{
m_instance = new Singleton(value);
}
}
return m_instance;
}
void Cat()
{
Singleton* s = Singleton::getInstance("cat");
cout << s->value() << "\n";
}
void Dog()
{
Singleton* s = Singleton::getInstance("Dog");
cout << s->value() << "\n";
}
int main()
{
thread t1(Cat);
thread t2(Dog);
t1.join();
t2.join();
return 0;
}
二、单例模式的优缺点
单例模式的优点:
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
单例模式的缺点:
- 违反了单一职责原则。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。