C++设计模式之单例模式

动机

在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。

如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?

这应该是类设计者的责任,而不是使用者的责任。

单例模式代码举例:把构造函数设置为private

cpp 复制代码
class Singleton{
private:
    Singleton();
    Singleton(const Singleton& other);
public:
    static Singleton* getInstance();
    static Singleton* m_instance;
};

Singleton* Singleton::m_instance=nullptr;

//线程非安全版本
Singleton* Singleton::getInstance() {
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
    Lock lock;
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}


//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    
    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}


//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

单例模式定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在单例模式中使用双检查锁(DCL,Double-Checked Locking)时,可能会遇到一些问题,特别是在多线程环境下。在C++11之前的版本中,由于缺少内存栅栏(memory barrier)的支持,可能导致DCL失效,从而造成线程安全性问题。

问题导致原因:

正确对象的创建过程是先调用构造对象,后进行内存分配,但是单例在进行创建对象时,在一些编译器和硬件架构中,对于指令的执行顺序可能会发生重排序,导致在实例化对象时,成员变量的内存分配可能在对象构造之前,从而使其他线程在获取到非空但尚未构造完全的对象。

在C++11及以后的版本中,引入了std::atomic等特性,可以更好地保证内存可见性和顺序性,从而避免了上述问题。因此,如果在C++11及以后的环境中,使用std::atomic可以解决DCL的一些问题。

cpp 复制代码
// Singleton.h
#pragma once

#include <atomic>
#include <mutex>

class Singleton {
public:
    // 获取单例实例的静态方法
    static Singleton* Instance();

private:
    // 私有构造函数,确保单例不能通过其他方式实例化
    Singleton() {}

private:
    // 使用std::atomic提供的原子操作,确保多线程环境下的安全性
    static std::atomic<Singleton*> m_instance;
    
    // 用于保护实例化过程的互斥锁
    static std::mutex m_mutex;
};

// Singleton.cpp
#include "Singleton.h"

// 初始化静态成员变量
std::atomic<Singleton*> Singleton::m_instance(nullptr);
std::mutex Singleton::m_mutex;

Singleton* Singleton::Instance() {
    // 使用std::atomic加载实例,确保内存可见性
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);

    if (tmp == nullptr) {
        // 使用互斥锁保护实例化过程
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);

        if (tmp == nullptr) {
            // 实例化单例对象
            tmp = new Singleton;

            // 使用内存顺序和std::atomic_thread_fence确保内存可见性和顺序性
            std::atomic_thread_fence(std::memory_order_release);
            
            // 存储单例对象的指针
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }

    return tmp;
}

这里,使用std::atomic提供的内存顺序和std::atomic_thread_fence来保证正确的内存可见性和顺序性。在C++11及以后的环境中,这种方式是相对安全的。

使用atomic类可以确保在多线程环境下,对共享数据的操作不会导致数据的错误修改或不一致的状态。这对于编写线程安全的并发代码非常重要。

相关推荐
阳光_你好42 分钟前
请详细说明opencv/c++对图片缩放
c++·opencv·计算机视觉
杰克逊的黑豹1 小时前
不再迷茫:Rust, Zig, Go 和 C
c++·rust·go
Y.O.U..1 小时前
今日八股——C++
开发语言·c++·面试
Zhichao_972 小时前
【UE5 C++课程系列笔记】33——商业化Json读写
c++·ue5
云边有个稻草人2 小时前
【C++】第八节—string类(上)——详解+代码示例
开发语言·c++·迭代器·string类·语法糖auto和范围for·string类的常用接口·operator[]
惊鸿一博2 小时前
c++ &&(通用引用)和&(左值引用)区别
开发语言·c++
高 朗3 小时前
2025高频面试设计模型总结篇
设计模式·面试·职场和发展
nuo5342023 小时前
黑马 C++ 学习笔记
c语言·c++·笔记·学习
DARLING Zero two♡3 小时前
C++类间的 “接力棒“ 传递:继承(上)
开发语言·c++·继承·里氏替换原则
会讲英语的码农3 小时前
如何学习C++以及C++的宏观认知
开发语言·c++·学习