定义

一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

实战案例

处理资源访问冲突

自定义实现一个往文件中打印日志的 Logger 类。

表示全局唯一类

从业务上讲,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。

比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。

再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器设计为单例。

实现一个单例

要实现一个单例,我们需要关注的点无外乎下面几个:

  • 构造函数需要是 private 访问权限,这样才能避免外部创建实例;

  • 考虑对象创建时的线程安全问题;

  • 考虑是否支持延迟加载;

  • 考虑 getInstance() 性能是否高(是否加锁)。

饿汉式

在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(真正用到的时候,再创建实例)。

#include <new>

class Singleton
{
public:
    Singleton(const Singleton& single) = delete;
    Singleton& operator=(const Singleton& single) = delete;

    static Singleton* GetInstance();

private:
    Singleton() = default;
    ~Singleton() = default;

    static Singleton* m_pInstance;

    class GC
    {
    public:
        ~GC()
        {
            if (m_pInstance != nullptr)
            {
                delete m_pInstance;
                m_pInstance = nullptr;
            }
        }
    };

    static GC gc;
};

Singleton* Singleton::m_pInstance = new (std::nothrow) Singleton;
Singleton::GC Singleton::gc;

Singleton* Singleton::GetInstance()
{
    return m_pInstance;
}

int main(int argc, char* argv[])
{
    Singleton* object = Singleton::GetInstance();

    return 0;
}

有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方式应该是用到的时候再去初始化。不过,我个人并不认同这样的观点。

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴漏),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

懒汉式

有饿汉式,对应的,就有懒汉式。懒汉式相对于恶汉式的优势是支持延迟加载。

普通懒汉式(线程不安全)

class Singleton 
{
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* GetInstance() 
    {
        if(m_pInstance == nullptr)
        {
            m_pInstance = new (std::nothrow) Singleton();
        }
        
        return m_pInstance;
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
    
    static Singleton *m_pInstance;
    
    class GC
    {
    public:
        ~GC() 
        {
            if(m_pInstance != nullptr)
            {
                delete m_pInstance;
                m_pInstance = nullptr;
            }
        }
    };
    
    static GC gc;
};

Singleton* Singleton::m_pInstance = nullptr;
Singleton::GC Singleton::gc;

加锁的懒汉式单例(线程安全)

class Singleton 
{
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* GetInstance() 
    {
        if(m_pInstance == nullptr)
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            if(m_pInstance == nullptr)
            {
                m_pInstance = new (std::nothrow) Singleton();
            }
        }
        
        return m_pInstance;
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
    
    static Singleton *m_pInstance;
    
    class GC
    {
    public:
        ~GC() 
        {
            if(m_pInstance != nullptr)
            {
                delete m_pInstance;
                m_pInstance = nullptr;
            }
        }
    };
    
    static GC gc;
    static std::mutex m_mutex;
};

Singleton* Singleton::m_pInstance = nullptr;
Singleton::GC Singleton::gc;
std::mutex Singleton::m_mutex;

实际上,cpu 指令重排序可能会导致问题。

p = new Singleton;

这条语句会导致三件事情的发生:

  1. 分配能够存储Singleton对象的内存;

  2. 在被分配的内存中构造一个Singleton对象;

  3. p指向这块被分配的内存。

一般情况下执行顺序是 123,但是指令重排序下执行顺序可能会变为 132。这种情况下,会导致线程 A 执行了 3,但是 2 还未执行,线程 B 判断 p 非空,但是实际上对象还未构造。

不过这个问题在 C++11 中得到了解决,因为新的 C++11 规定了新的内存模型,保证执行上述 3 个步骤的时候不会发生线程切换,相当于这个初始化过程是"原子性"的操作(后续补充)。

内部静态变量的懒汉式

class Singleton
{
public:
    Singleton(const Singleton& single) = delete;
    Singleton& operator=(const Singleton& single) = delete;
    
    static Singleton* get_instance()
    {
        static Singleton instance;
        return &instance;
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
};

int main()
{
    Singleton* object = Singleton::get_instance();
    
    return 0;
}

单例模式宏

#define SINGLETON_DEFINE(TypeName)        \
static TypeName* GetInstance()            \
{                                         \
    static TypeName type_instance;        \
    return &type_instance;                \
}                                         \
TypeName(const TypeName&) = delete;      \
TypeName& operator=(const TypeName&) = delete;    \

单例存在哪些问题

单例对 OOP 特性的支持不友好

通过 IdGenerator 这个例子来讲解

class Order {
public:
    void create(...) {
        // ...
        long id = IdGenerator.GetInstance().GetId();
        // ...
    }
};

class User {
public:
    void create(...) {
        // ...
        long id = IdGenerator.GetInstance().GetId();
        // ...
    }
};

IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,我们希望针对不同业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码改动就会比较大。

class Order {
public:
    void create(...) {
        // ...
        long id = OrderIdGenerator.GetInstance().GetId();
    }
};

public User {
public:
    void create(...) {
        long id = UserIdGenerator.GetInstance().GetId();
    }
};

除此之外,单例对继承、多态特性的支持也不友好。

单例会隐藏类之间的依赖关系

单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

单例对代码的扩展性不友好

单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

比如,数据库连接池,需要区分慢 SQL 和其他 SQL,我们希望可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

单例不支持有参数的构造函数

解决方案:

将参数放到另外一个全局变量中。Config 是一个存储了 paramA 和 paramB 值的全局变量。里面的值既可以通过静态常量定义,也可以额从配置文件中加载得到。

struct Config {
    const static int PARAM_A = 123;
    const static int PARAM_B = 245;
};

class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* GetInstance() {
        static Singleton instance;
        return &instance;
    }

private:
    Singleton()
        : m_paramA(Config::PARAM_A)
        , m_paramB(Config::PARAM_B)
    {}

    ~Singleton() = default;

    int m_paramA;
    int m_paramB;
};

int main() {
    Singleton* object = Singleton::GetInstance();

    return 0;
}

单例的替代方案

为了保证全局唯一,除了使用单例,还可以用静态方法来实现。

#include <atomic>
#include <iostream>

class IdGenerator {
public:
    static long GetId() {
        return id.fetch_add(1);
    }

private:
    static std::atomic_long id;
};

std::atomic_long IdGenerator::id = 0;

int main() {
    std::cout << IdGenerator::GetId() << std::endl;
}

不过,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,无法支持延迟加载。我们再来看看有没有其他办法。

// 依赖注入
void DemoFunction(IdGenerator* idGenerator) {
    long id = idGenerator->GetId();
}

IdGenerator* idGenerator = IdGenerator::GetInstance();
DemoFunction(idGeneator);

基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间的依赖关系的问题。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性不友好等问题,还是无法解决。

总结

如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

参考:王争 设计模式之美