单例模式的原理/懒汉模式/饿汉模式以及不同版本的单例模式程序

本文详细介绍了单例模式的定义,包括懒汉模式和饿汉模式的原理及区别。懒汉模式在首次使用时初始化,饿汉模式在类加载时即完成初始化。懒汉模式可能存在线程安全问题,文章列举了多个版本的懒汉模式实现,分析了各自存在的问题,如内存泄漏和线程不安全。最后提出了一种利用C++11静态局部变量特性的线程安全且自动管理内存的单例实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例模式定义

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

从初始化的时间角度上看,单例模式分为两种,懒汉模式饿汉模式

懒汉模式与饿汉模式

单例模式需要将构造和析构私有化,让其他用户不能调用; 例如拷贝,移动等构造函数都不能被调用

懒汉模式

class Singleton {
public:
    static Singleton * GetInstance() {
    //在使用的时候才会初始化单例对象,并且只初始化一次
        if (_instance == nullptr) {
            _instance = new Singleton();
        }
        return _instance;
    }
private:
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉	构造
    Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
    static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

饿汉模式

class Singleton {
public:
    static Singleton * GetInstance() {
        return _instance;
    }
private:
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉	构造
    Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
    static Singleton * _instance;
};
Singleton* Singleton::_instance = new Singleton();//静态成员需要初始化即生成单例对象

懒汉模式和饿汉模式的区别

直观区别
饿汉:饿汉模式就是类在加载时,单例初始化便完成,保证GetInstance的时候,单例是已经存在了。
懒汉:懒汉比较懒,只有当调用GetInstance的时候,才会初始化单例对象。

性能上的区别
饿汉:在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。
懒汉:由于会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉模式一样了。
如果这个创建过程很耗时,比如需要连接多次数据库、传输大量文件等;并且这个类还并不一定会被使用,那么这个创建过程就是无用的。此时懒汉会比饿汉更适合,具体那个模式更好还要看应用场景决定。

线程安全
饿汉:是线程安全的,可以直接用于多线程而不会出现问题。

懒汉:本身是非线程安全的。

懒汉模式的不同版本

版本一

class Singleton {
public:
	static Singleton * GetInstance() {
		if (_instance == nullptr) {
		_instance = new Singleton();
	}
		return _instance;
	}
private:
	Singleton(){}; //构造
	~Singleton(){};
	Singleton(const Singleton &) = delete; //拷⻉	构造
	Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
	Singleton(Singleton &&) = delete;//移动构造
	Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
	static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

存在问题
_instance是静态全局区分配的内存。程序退出时_instance这个指针 可以释放;但是_instance这个指针指向的堆上的资源(new Singleton())是没法回收的,没有delete。

注意:delete也不能放在析构函数中,因为delete会调用析构函数,析构函数又有delete,又调用析构函数,导致无限递归析构。

问题1:上面的程序在退出时,不会去析构_instance。造成内存泄漏,可以使用unique_ptr智能指针解决
问题2:线程不安全

版本二

class Singleton {
public:
    static Singleton * GetInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
            atexit(Destructor);//使用atexit调用Destructor函数释放内存
        }
        return _instance;
    }
private:
    static void Destructor() {
        if (nullptr != _instance) { //
            delete _instance;
            _instance = nullptr;
        }
    }
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
    static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
// 还可以使⽤ 内部类,智能指针来解决; 但此时还有线程安全问题

存在问题:在多线程下,可能两个线程同时进入 if (_instance == nullptr)指令,同时初始化单例线程,造成线程不安全

版本三

#include <mutex>
class Singleton { // 懒汉模式 lazy load
public:
    static Singleton * GetInstance() {
        if (_instance == nullptr) {
            std::lock_guard<std::mutex> lock(_mutex); // 3.2双重检测可以避免第一次读的时候多个线程进入的问题。
            if (_instance == nullptr) {
                _instance = new Singleton();
                //加锁只是考虑C++98时单核时代的多线程情况,而C++11的多核时代可能会有重排机制,同样会有线程安全问题
                atexit(Destructor);
            }
        }
        return _instance;
    }
private:
    static void Destructor() {
        if (nullptr != _instance) {
            delete _instance;
            _instance = nullptr;
        }
    }
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉构造
    Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
    Singleton(Singleton &&) = delete;//移动构造
    Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
    static Singleton * _instance;
    static std::mutex _mutex;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化

通过加锁来解决多线程时的线程安全问题,是可以的。


但是执行operator new命令时的步骤:

  1. 分配内存空间
  2. 调用构造函数初始化对象
  3. 返回指针,也就是设置_instance指向刚刚分配的内存的地址

其中2和3步骤没有依赖关系,可以先初始化对象,再设置指针指向地址;也可以先设置地址,再初始化对象。


如果是单线程下调用顺序1->2->3,没有问题

但是c++11的多核时代会有一些优化操作,例如编译器重排 ,CPU重排等;
重排机制可能会违反顺序一致性
可能会调用顺序1->3->2。因此当执行完步骤3后,_instance已经不为空了,这时虽然还没有初始化,但是其他线程已经可以使用这个还没有初始化的对象了,也造成线程安全问题。

存在问题:执行new Singleton()时,重排优化机制导致的线程安全问题。

版本四

#include <iostream>
using namespace std;
class Singleton
{
public:
    static Singleton* GetInstance() {
        static Singleton* instance = new Singleton();
        return instance;
    }
private:
    Singleton(){}; //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷⻉  构造
    Singleton& operator=(const Singleton&) =
            delete;//拷贝赋值构造
            Singleton(Singleton &&) = delete;//移动构造
            Singleton& operator=(Singleton &&) =
                    delete;//移动拷贝构造
};
int main() {
    Singleton* test1 = Singleton::GetInstance();
    cout << test1 << endl;
    Singleton* test2 = Singleton::GetInstance();
    cout << test2 << endl;
    return 0;
}
// 继承 Singleton
// ubuntu下使用使用C++11新特性编译程序方式:g++ Singleton.cpp -o singleton -std=c++11

该版本具备的优点

  1. 利⽤静态局部变量特性,延迟加载;
  2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函 数;
  3. 静态局部变量初始化时,没有 new 操作带来的cpu指令 重排reorder操作;在C++11之后,局部静态变量是线程安全的。
  4. c++11 静态局部变量初始化时,具备线程安全;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值