单例模式

单例模式可能是最简单,最常用的设计模式之一,这篇文章主要总结一些常用的实现方式

单例是什么

单例模式(Singleton), 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

应用场景

当我们需要统一管理资源或者资源共享的时候都可以使用单例模式

  • 管理资源:多线程/数据库 线程池,资源管理器等等
  • 资源共享:读写应用配置,日志应用等等

代码实现说明

  • 为了保证全局只有一个实例,需要使用 static, 同时需要防止被外界创建和赋值,就需要私有化它的构造函数,并且禁止赋值和拷贝
  • 提供一个公有的静态方法获取该实例

而关于什么时候初始化这个单例,实际上有分为2种

类型 描述
饿汉模式 变量在声明时便初始化
懒汉模式 先声明一个空变量,用户调用的时候才初始化

饿汉模式

饿汉模式 像一个饿汉一样,不管需不需要用到实例都要去创建实例,变量在声明时便初始化,所以可以有以下的实现方法

class EagerSingleton
{
public:
    static EagerSingleton& getInstance(){return instance;}

private:
    EagerSingleton() { std::cout << "EagerSingleton create" << std::endl; }
    ~EagerSingleton(){}
    EagerSingleton(const EagerSingleton&)=delete;
    EagerSingleton& operator=(const EagerSingleton&)=delete;

private:
    static EagerSingleton instance;
};

EagerSingleton EagerSingleton::instance;

将构造,赋值,复制,析构都设置成私有函数,暴露一个 getInstance 接口,返回静态成员对象 instance 供开发人员调用

当然这里静态变量 instance 也可以设计成指针, 只要保证在声明时初始化即可例如

class EagerSingleton
{
public:
    static EagerSingleton* getInstance(){return instance;}
    ...
private:
    static EagerSingleton* instance;
};

EagerSingleton* EagerSingleton::instance = new EagerSingleton;

由于在 main() 函数之前初始化,所以没有线程安全的问题,但是这里存在2个可能存在问题

过早的初始化

因为从 main() 函数之前就初始化,但是程序可能全程没有用到 EagerSingleton 这个类,可以完全不用这么早初始化,等用的时候在初始化就可以

之前看 《More Effective C++》 第17条款的时候,提到过 lazy evaluation(缓式评估), 从效率而言,最好的运算是从未被执行的运算,如果没用到,完全可以不去做,能拖就拖,拖到必须做,必须迫切需要的时候再去执行,这就引入了单例的另一种实现–懒汉模式

non-local static 引入的潜在风险

先看看什么是 non-local static

下面内容摘自 《Effective C++》 的条款04

函数内的 static 对象称为 local static 对象(因为它们对函数而言是 local),其他 static 对象被称为 non-local static

如果某个编译单元内某个 non-local static 对象的初始化动作使用了另一个编译单元的某个 non-local static 对象,它所用到的这个对象可能尚未被初始化, 因为 C++ 对 “定义于不同编译单元内的 non-local static 的初始化次序并无明确定义”, 主要是决定它们的初始化顺序相当困难,根本无解

当然这种情况在实际编写代码的时候可能很少出现,并且书中也提出一个小小的设计来完全消除这个问题

以 local static 对象替换 non-local static 对象

具体关于这种设计的说明,后面代码里有介绍,这里就先不展开了

懒汉模式

懒汉模式,像一个懒汉一样,需要用到该对象的时候再去创建实例,不需要创建实例程序就“懒得”去创建实例,相对于饿汉模式还没开始(mian函数)就初始化显得更加合理

从未被调用,就不用承担构造和析构的成,其实换句话可以这样理解

  • 饿汉模式是拿空间换时间做法
  • 懒汉模式是拿时间换空间做法

基础版

class LazySingletonDemo
{
private:
    static LazySingletonDemo* instance;

private:
    LazySingletonDemo() { std::cout << "LazySingletonDemo create" << std::endl;}
    ~LazySingletonDemo() {}
    LazySingletonDemo(const LazySingletonDemo&)=delete;
    LazySingletonDemo& operator=(const LazySingletonDemo&)=delete;
public:
    static LazySingletonDemo* getInstance()
    {
        if(instance == nullptr)
            instance = new LazySingletonDemo();
        return instance;
    }
};

LazySingletonDemo* LazySingletonDemo::instance = nullptr;

调用的时候才初始化,程序单线程调用时是没有问题的,但是多线程系统中就存在不确定性了,可能因为多个线程同时请求初始化导致的内存泄漏问题

所以问题来到了如何消除与初始化有关的 “竞速形势”

最简单的就是初始化前加锁,保证创建这个过程是唯一的,好,就有下面这种改进版

双检查锁 Double-Checked Locking Pattern

#include <iostream>
#include <mutex>

class LazySingletonMore
{
private:
    static LazySingletonMore* instance;
    static std::mutex mutex;

private:
    LazySingletonMore() { std::cout << "LazySingletonMore create" << std::endl;}
    ~LazySingletonMore() {}
    LazySingletonMore(const LazySingletonMore&)=delete;
    LazySingletonMore& operator=(const LazySingletonMore&)=delete;
public:
    static LazySingletonMore* getInstance()
    {
        if(instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex);
            if(instance == nullptr)
                instance = new LazySingletonMore();
        }

        return instance;
    }
};

LazySingletonMore* LazySingletonMore::instance = nullptr;
std::mutex LazySingletonMore::mutex;
  • 第一步判断,如果已经创建了,直接返回
  • 第二步加锁,如果多个线程同时进入了初始化环节,加锁保证同步线程
    • 拿到锁的线程,因为未初始化,创建实例,创建结束,同步结束
    • 同步结束后,其他未执行初始化的线程,因为已经初始化了,就不用再创建了。

C++11推荐方式

#include <iostream>
using namespace std;

class Singleton
{
public:
    static Singleton& getInstance()
    {
        static Singleton value;
        return value;
    }

private:
    Singleton() = default;
    Singleton(const Singleton& other)=delete;
    Singleton& operator=(const Singleton&)=delete;
};

这个手法的基础在于: C++保证,函数中的 local static 对象会在 “该函数被调用期间” “首次遇上该对象之定义式” 时被初始化,所以如果以 函数调用 返回一个 reference 指向 local static 对象 替换 直接访问 non-local static 对象,就可以获得保证,保证所获得的那个 reference 将指向一个 历经初始化 的对象

不过 local static 线程安全是 C++11 之后才支持的,在 C++11 之前,在多线程环境下local static 对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行 local static 对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该 local static 对象的构造函数中。这会造成这个 local static 对象的重复构造,进而产生 内存泄露 问题。

总结

虽然很常用,但是在保证线程安全的前提下,实现起来考虑的细节还是挺多的,

还有一些细节的知识,建议看一下下面几篇博客,里面提了一些很有意思的思考

  • 通过C++11提供的 call_once 来实现单例
  • getInstance 到底应该返回指针还是引用
  • 如果想复用的话,利用模板应该如何实现等等

https://www.cnblogs.com/loveis715/archive/2012/07/18/2598409.html

https://zhuanlan.zhihu.com/p/37469260

https://zhuanlan.zhihu.com/p/62014096

关于单例的总结大概也就这么多了