C++单例模式详解

C++线程安全的单例模式详解

单例模式

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

为什么要使用单例模式

  • 处理资源冲突。
  • 表示全局唯一类。

如何实现一个单例

实现单例的方式有很多。但概括起来,要实现一个单例,我们需要关注的点无外乎下面几个:

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例。当然,如果你想继承单例,可以将构造函数声明为 protected;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁)。

饿汉式

在饿汉式单例中,实例在类加载时就被创建,而不是在第一次使用时才创建。以下是一个使用饿汉式实现的单例模板:

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        return instance;
    }

    // 防止复制构造函数和赋值运算符被调用
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 在类加载时创建单例实例
    static T instance;

    // 私有构造函数,防止外部实例化
    Singleton() {}
};

template <typename T>
T Singleton<T>::instance; // 初始化单例实例

这个模板类可以用于创建其他类的饿汉式单例。使用示例如下:

class MySingleton {
public:
    void doSomething() {
        // 实现单例类的功能
    }

    // 其他成员函数...

private:
    // 私有构造函数,防止外部实例化
    MySingleton() {}

    // 其他私有成员变量...
    // Singleton 模板类作为友元,以便访问私有构造函数
    friend class Singleton<MySingleton>;    
};

// 使用单例模板创建 MySingleton 的单例
using MySingletonInstance = Singleton<MySingleton>;

int main() {
    MySingleton& instance = MySingletonInstance::getInstance();
    instance.doSomething();

    return 0;
}

在这个示例中,Singleton 模板类确保了在程序加载时创建了单例实例,因此这是一个饿汉式单例模式的实现。

懒汉式

双重检查锁定

先看一个错误的实现方式。

#include <iostream>
#include <mutex>

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        // 使用双重检查锁定(Double-Checked Locking)来确保线程安全
        if (!instance) {
            std::lock_guard<std::mutex> lock(mutex);
            if (!instance) {
                instance = new T();
            }
        }
        return *instance;
    }

    // 防止复制构造函数和赋值运算符被调用
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 私有构造函数,防止外部实例化
    Singleton() {}

    static T* instance;
    static std::mutex mutex;
};

template <typename T>
T* Singleton<T>::instance = nullptr;

template <typename T>
std::mutex Singleton<T>::mutex;

咋一看,似乎没有问题。但是存在着访问未初始化对象的可能。对于instance = new T()实际上是可以分为两步:

instance = operator new(sizeof(T));
new (instance) T();

如果线程A在执行完第一步时,切换到线程B。线程B就会认为instance已经完成初始化,然后获取instance句柄,执行后续操作。这明显存在潜在的风险。

而使用 std::atomic 结合 std::mutex 是解决双重锁定问题的一种有效方式。这可以确保在多线程环境中正确地初始化单例对象,同时保持性能。以下是使用 std::atomicstd::mutex 的双重检查锁定的懒汉式单例模板实现:

#include <iostream>
#include <mutex>
#include <atomic>

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        if (!instance.load(std::memory_order_acquire)) {
            std::lock_guard<std::mutex> lock(mutex);
            if (!instance.load(std::memory_order_relaxed)) {
                T* temp = new T;
                instance.store(temp, std::memory_order_release);
            }
        }
        return *instance.load(std::memory_order_relaxed);
    }

    // 防止复制构造函数和赋值运算符被调用
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 私有构造函数,防止外部实例化
    Singleton() {}

    static std::atomic<T*> instance;
    static std::mutex mutex;
};

template <typename T>
std::atomic<T*> Singleton<T>::instance(nullptr);

template <typename T>
std::mutex Singleton<T>::mutex;

在上述实现中,我们使用 std::atomic 来管理单例对象的指针,确保在多线程环境中对其进行正确的加载和存储。同时,我们使用 std::mutex 进行互斥锁定以确保初始化代码的线程安全性。

这个实现结合了懒汉式和双重检查锁定的优势,是一种高效且线程安全的方式来实现单例模式。

C++11 static 局部变量

要确保线程安全的单例模式,更好的方法是使用C++11引入的局部静态变量(Local Static Variable)。以下是使用局部静态变量实现的懒汉式单例模板:

#include <iostream>
#include <mutex>

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance; // 使用局部静态变量
        return instance;
    }

    // 防止复制构造函数和赋值运算符被调用
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 私有构造函数,防止外部实例化
    Singleton() {}
};

这个示例中,通过使用局部静态变量,我们可以确保在第一次请求实例时才进行初始化,而且不需要显式使用互斥锁,因为C++11规定局部静态变量的初始化是线程安全的。

使用示例如下:

class MySingleton {
public:
    void doSomething() {
        // 实现单例类的功能
    }

    // 其他成员函数...

private:
    // 私有构造函数,防止外部实例化
    MySingleton() {}

    // 其他私有成员变量...

	// Singleton 模板类作为友元,以便访问私有构造函数
  	friend class Singleton<MySingleton>;
};

// 使用单例模板创建 MySingleton 的单例
using MySingletonInstance = Singleton<MySingleton>;

int main() {
    MySingleton& instance = MySingletonInstance::getInstance();
    instance.doSomething();

    return 0;
}

这种方式是更加安全和简单的懒汉式单例模式的实现方式,建议在C++11及更高版本的环境中使用。

饿汉式 vs. 懒汉式

  • 创建或初始化时机:在饿汉式单例中,实例在程序启动时就被创建,而不是在第一次使用时才创建。而对于懒汉式,是延迟到在第一次请求实例时才进行初始化;
  • 性能和内存开销:在饿汉式单例中,无论是否使用单例,均要创建并初始化对象。而懒汉式虽然延迟了对象的创建和初始化,但在getInstance接口性能上有略微劣势。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值