参考:flamingo即时通信软件
单例模式
单例模式是创建型设计模式,指的是在系统的生命周期中只能产生一个实例,确保该类的唯一性。
单例模式的使用场景主要是:写进程池类、日志类、内存池等。
如何创建单例模式
我们希望一个类在整个程序中只保存一份实例,也就是说不能向外提供这个类的构造函数和析构函数,那么我们只能将构造函数和析构函数定义为私有。
class Singleton
{
private:
Singleton(){}
~Singleton(){}
}
那要怎么才能在没有显式实例化对象的情况下,获取到这唯一的一个实例呢?我们知道,类中定义的public static函数可以脱离对象,直接调用,因此现在只能通过这个方法来得到对象了。
class Singleton
{
public:
static Singleton* GetInstance()
{
Singleton* instance = new Singleton();
return instance;
}
private:
Singleton(){}
~Singleton(){}
}
int main()
{
// 调用static函数得到对象
Singleton* instance = Singleton::GetInstance();
return 0;
}
这个代码有问题吗?直接new一个对象返回不就行了?有问题,我们希望只实例化一次对象,但是这个函数显然每次调用都会实例化一次对象。
所以我们需要一个变量来判断当前对象是否已经存在。由于static函数只能访问static成员函数或者变量(构造函数、析构函数除外),因此需要记录一下当前是否已经实例化了这个对象。
class Singleton
{
public:
static Singleton* GetInstance()
{
if (instance == nullptr)
{
instance = new Singleton();
}
return instance;
}
private:
Singleton(){}
~Singleton(){}
static Singleton* instance;
}
// 需要注意,类中的static成员变量必须在类之外初始化
Singleton* Singleton::instance = nullptr;
int main()
{
// 调用static函数得到对象
Singleton* instance = Singleton::GetInstance();
return 0;
}
再将类的拷贝构造和赋值构造函数设置为删除,防止外部调用。并且提供一个进程结束时调用的函数,防止内存泄漏。
class Singleton
{
public:
static Singleton* GetInstance()
{
if (instance == nullptr)
{
instance = new Singleton();
}
return instance;
}
static void deleteInstance()
{
delete instance;
}
private:
Singleton(){}
~Singleton(){}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
static Singleton* instance;
};
// 需要注意,类中的static成员变量必须在类之外初始化
Singleton* Singleton::instance = nullptr;
int main()
{
// 调用static函数得到对象
Singleton* instance = Singleton::GetInstance();
return 0;
}
这样就是单例模式的模板了。总结一下要点:
- 全局只有一个实例,用static特性实现,构造函数设为私有
- 通过static接口获得实例
- 禁止拷贝和赋值
懒汉模式和饿汉模式
上面的单例模式在单线程下是安全的,但是在多线程、多进程下存在问题。
就比如多线程下,在判断instance == nullptr
的时候,多个线程可能同时进入,多次实例化。
由这个问题引申出了单例模式的两种模式:懒汉式和饿汉式。
懒汉式:只有当需要这个实例的时候才会创建并使用实例。这种方式会出现上面描述的线程安全问题。
饿汉式:主程序开始运行(还未创建线程)时就创建实例。这种方式不会出现线程问题。
懒汉式可以有两种解决方式:
c++11标准中有一个特性:如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。因此这种懒汉式是最推荐的,有如下要点:
(1)通过局部静态变量的特性保证了线程安全
(2)不需要使用共享指针和锁
(3)get_instance()函数要返回引用而尽量不要返回指针
// 局部静态变量的懒汉模式
class Singleton
{
public:
~Singleton(){}
static Singleton& get_instance(){
//关键点!不推荐返回指针的方式
static Singleton instance;
return instance;
}
private:
Singleton(){}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
使用锁、共享指针实现的懒汉单例模式。有以下要点:
(1)基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
(2)加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
不足之处是,使用锁也有开销,并且代码写起来比较复杂
// 共享指针和锁的懒汉模式
#include <iostream>
#include <memory> // shared_ptr
#include <mutex> // mutex
class Singleton {
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton() {}
static Ptr get_instance() {
// 双检锁
if (m_instance_ptr == nullptr) {
std::lock_guard<std::mutex> lk(m_mutex);
if (m_instance_ptr == nullptr) {
m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_instance_ptr;
}
private:
Singleton() {}
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Ptr m_instance_ptr;
static std::mutex m_mutex;
};
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
关于双检锁,有下面的解释,看完应该能懂了:
在上面的代码中,第一次判断
m_instance_ptr == nullptr
的作用是为了避免在单例对象已经被创建的情况下重复获取锁和创建对象。如果单例对象已经被创建,则直接返回单例对象的指针。
而第二次判断m_instance_ptr == nullptr
的作用是为了在获取锁之后再次检查单例对象是否已经被创建。这个判断是必要的,因为多个线程可能同时通过第一次检查,但只有一个线程能够获取锁并创建单例对象,其他线程需要等待这个线程释放锁后才能继续执行。在等待的过程中,其他线程可能会获取到锁,因此需要再次检查单例对象是否已经被创建。
总之,第一次判断可以避免重复创建单例对象,提高程序的性能;第二次判断可以保证在多线程环境下安全地创建单例对象。双重检查锁定是一种常见的单例模式实现方式,它可以同时满足线程安全和性能的要求。
面试题
- 懒汉模式和饿汉模式的实现(判空!!!加锁!!!),并且要能说明原因(为什么判空两次?)
回答双检锁的意义
- 构造函数的设计(为什么私有?除了私有还可以怎么实现(进阶)?)
(1)可以设置为protected(废话)
(2)可以设置为public,但是要通过工厂模式来构造实例对象。(待填坑,感觉flamingo就是工厂模式)
- 对外接口的设计(为什么这么设计?)
必须设置为public static,这样外部才能调用
- 单例对象的设计(为什么是static?如何初始化?如何销毁?(进阶))
因为对外接口是static的,所以单例对象只能设置为static。要销毁单例对象,必须对外提供一个public static函数接口,执行delete操作,但是要注意线程安全问题,最好是在线程全部退出,主进程结束之前调用。
- 对于C++编码者,需尤其注意C++11以后的单例模式的实现(为什么这么简化?怎么保证的(进阶))
直接定义static局部变量(非指针),因为只会初始化一次,并且线程安全
源码分析
在这个项目中,使用了一个模板类来实现单例模式。
这段代码实现了一个模板类Singleton,可以用于实现单例模式。单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点。
在Singleton类中,Instance()函数是一个静态函数,用于获取单例对象的引用。
如果单例对象还没有创建,则在第一次调用Instance()函数时创建它,并在后续调用时返回该对象的引用。
该类的实现使用了一个静态指针value_来保存单例对象的指针,并将其初始化为nullptr。
在第一次调用Instance()函数时,如果value_为nullptr,则创建一个新的单例对象并将其赋值给value_,然后返回该对象的引用。在后续调用中,直接返回value_指向的单例对象。
init()函数和destroy()函数是私有函数,分别用于创建单例对象和销毁单例对象。
由于Singleton类是一个模板类,所以它的构造函数和析构函数是公共函数,但是拷贝构造函数和赋值运算符被禁用,以确保单例对象只有一个实例。
该代码中还包含了一些注释掉的代码,这些代码是用于在多线程环境下安全创建单例对象的。
其中,pthread_once()函数可以确保在多线程环境下只调用一次init()函数,而atexit()函数可以在程序结束时自动调用destroy()函数来销毁单例对象。
需要注意的是,这些代码可能会因操作系统和编译器的不同而有所不同,因此在使用时需要根据实际情况进行调整。
template<typename T>
class Singleton
{
public:
static T& Instance()
{
//pthread_once(&ponce_, &Singleton::init);
if (nullptr == value_)
{
value_ = new T();
}
return *value_;
}
private:
Singleton();
// 调用默认的析构函数
~Singleton() = default;
// 取消复制构造函数
Singleton(const Singleton&) = delete;
// 取消赋值构造函数
Singleton& operator=(const Singleton&) = delete;
static void init()
{
value_ = new T();
//::atexit(destroy);
}
static void destroy()
{
delete value_;
}
private:
//static pthread_once_t ponce_;
static T* value_;
};
//template<typename T>
//pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = nullptr;