[C/C++] -- 单例模式

1.简介

单例模式是一种创建型设计模式,它确保类只有一个实例,并提供全局访问点以访问该实例。

引入单例模式目的:

  • 全局访问点: 单例模式提供了一个全局访问点,使得任何地方都可以方便地访问该实例,而不需要通过传递对象的方式。

  • 节省资源: 在某些情况下,创建类的实例可能会消耗较多的资源,例如数据库连接、线程池等。通过使用单例模式,可以避免频繁地创建和销毁对象,从而节省资源和提高性能。

  • 确保唯一性: 单例模式确保类的实例只有一个,避免了因多次创建实例而导致的数据不一致或状态不同步的问题。

  • 控制实例化过程: 单例模式将类的实例化过程集中在一个地方,使得可以更加灵活地控制实例化的时机和方式,例如延迟实例化、懒加载等。

  • 提供全局服务: 单例模式常用于提供全局服务或管理全局状态,例如日志记录器、配置管理器、线程池等。

线程安全

在多线程环境中,确保单例模式的线程安全性至关重要。如果不考虑线程安全性,在多个线程同时调用 getInstance() 方法时,可能会导致多个实例被创建,违反了单例模式的原则。

在C++中,可以通过以下几种方式来确保单例模式的线程安全性:

  • 使用静态初始化: 在C++11及以后的标准中,静态局部变量的初始化是线程安全的。因此,可以将单例实例声明为一个静态局部变量,并在 getInstance() 方法中返回该变量。这样做可以保证在首次调用 getInstance() 方法时,单例实例会被线程安全地初始化。

  • 加锁: 可以使用互斥量(mutex)来实现加锁机制,确保在同一时间只有一个线程可以执行关键代码段。在 getInstance() 方法中,使用互斥量对实例化过程进行加锁,从而确保在多线程环境中只有一个实例被创建。

  • 双重检查锁定(Double-Checked Locking): 双重检查锁定是一种常见的在多线程环境下实现延迟初始化的方法。在 getInstance() 方法中,首先检查实例是否已经被创建,如果尚未创建,则使用互斥量进行加锁,并再次检查实例是否已经被创建。这种方式可以减少在每次调用 getInstance() 方法时都进行加锁的开销。

  • 使用原子操作: C++11引入了一系列的原子操作,如 std::atomic 类和相关的原子操作函数,可以在不需要显式加锁的情况下实现线程安全的操作。可以将单例实例声明为原子类型,从而确保对实例的访问是线程安全的。

2.单例模式实现

单例模式常见以下两类

饿汉式单例模式:还没有获取实例对象,实例对象已经产生

懒汉式单例模式:唯一的实例对象,直到第一次获取它的时候才产生

饿汉式单例模式

在类加载的时候就创建实例,保证了线程安全,但可能会造成资源浪费,因为实例在程序运行期间始终存在,即使没有被使用。

cpp 复制代码
class Singleton
{
public:
    static Singleton *getInstance() //3.获取类的唯一实例对象的接口方法
    {
        return &instance;
    }
private:
    static Singleton instance;      //2.定义一个唯一的类的实例对象
    Singleton()                     //1.构造函数私有化
    {

    }
    //确保单例模式只有一个实例存在
    Singleton(const Singleton &) = delete;//禁用拷贝构造函数
    Singleton &operator=(const Singleton &) = delete;//禁用拷贝赋值运算符
};
Singleton Singleton::instance;      //静态成员变量在类外初始化

int main()
{
    Singleton *p1 = Singleton::getInstance();
    Singleton *p2 = Singleton::getInstance();
    Singleton *p3 = Singleton::getInstance();

    //Singleton t = *p1;无法访问,因为拷贝构造已被禁用
    return 0;
}

懒汉式单例模式

在第一次使用时才进行实例化,避免了资源浪费,但需要考虑线程安全性。常见的线程安全的懒汉式实现方式包括使用加锁的方式或双重检查锁定。

cpp 复制代码
//懒汉式单例:唯一的实例对象,直到第一次获取它的时候才产生
std::mutex mtx;
//懒汉式单例模式-》是不是线程安全的呢?-》线程安全的懒汉式单例模式
class Singleton
{
public:
    //是不是可重入函数?    锁+双重判断
    static Singleton *getInstance() //3.获取类的唯一实例对象的接口方法
    {
        //lock_guard<std::mutex> guard(mtx);//锁的粒度太大了
        if(instance==nullptr)
        {
            lock_guard<std::mutex> guard(mtx);
            if(instance ==nullptr)
            {
                /*
                开辟内存
                构造对象
                给instance赋值
                */
                instance = new Singleton();
            }
        }
        return instance;
    }
private:
    //volatile多线程环境获取到的都是内存的值,变量值可以及时更新
    static Singleton *volatile instance;      //2.定义一个唯一的类的实例对象
    Singleton()                     //1.构造函数私有化
    {

    }
    //确保单例模式只有一个实例存在
    Singleton(const Singleton &) = delete;//禁用拷贝构造函数
    Singleton &operator=(const Singleton &) = delete;//禁用拷贝赋值运算符
};
Singleton*volatile Singleton::instance=nullptr;      //静态成员变量在类外初始化

int main()
{
    Singleton *p1 = Singleton::getInstance();//第一次进来才new对象
    Singleton *p2 = Singleton::getInstance();
    Singleton *p3 = Singleton::getInstance();

    return 0;
}

不使用互斥锁的线程安全的单例模式

C++11及之后的标准中,静态局部变量的初始化会在多线程环境下进行线程安全的操作,避免了竞态条件。因此,尽管没有显式地使用互斥锁来保护临界区,但由于静态局部变量的初始化机制,仍然确保了在多线程环境下只有一个实例被创建。

cpp 复制代码
class Singleton
{
    static Singleton *getInstance() //3.获取类的唯一实例对象的接口方法
    {
        //g++ -o run 单例模式.cpp -g gdb run
        //函数静态局部变量初始化,在汇编指令上已经自动添加线程互斥指令了
        static Singleton instance;//静态成员变量在数据段上,只有第一次调用时才初始化,对象初始化调用构造函数
        return &instance;
    }
private:
    Singleton()                     //1.构造函数私有化
    {
        //很多初始化代码
    }
    //确保单例模式只有一个实例存在
    Singleton(const Singleton &) = delete;//禁用拷贝构造函数
    Singleton &operator=(const Singleton &) = delete;//禁用拷贝赋值运算符
};

int main()
{
    Singleton *p1 = Singleton::getInstance();//第一次进来才new对象
    Singleton *p2 = Singleton::getInstance();
    Singleton *p3 = Singleton::getInstance();

    return 0;
}
相关推荐
paopaokaka_luck7 分钟前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
我们的五年10 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
以后不吃煲仔饭19 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师20 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
The_Ticker25 分钟前
CFD平台如何接入实时行情源
java·大数据·数据库·人工智能·算法·区块链·软件工程
程序猿阿伟26 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
大数据编程之光1 小时前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
ExiFengs1 小时前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring