C++设计模式学习笔记:单例模式

本文探讨C++中的单例模式,包括懒汉模式和饿汉模式。懒汉模式强调延迟加载,但在多线程环境下可能产生线程安全问题和内存泄漏。文中列举了两种懒汉模式的实现,分析了其潜在问题。饿汉模式则确保了类的初始化在多线程环境中的安全性。

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

1.懒汉模式

懒汉模式的特点是延迟加载,比如配置文件,采用懒汉式的方法,配置文件的实例直到用到的时候才会加载,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。以下是懒汉模式实现方式C++代码:

(1)懒汉模式实现一:静态指针 + 用到时初始化

//代码实例(线程不安全)
template<typename T>
class Singleton
{ 
public: 
	static T& getInstance()
	{     
		if (!value_)     
		{         
			value = new T();
	    }     
	    return *value; 
	}
private:     
	Singleton();   
	~Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;  
private:
	static T* value; 
}; 
template<typename T> 
T* Singleton<T>::value = nullptr;

在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了,该方法是线程不安全的。
a. 假如线程A和线程B,这两个线程要访问getInstance函数,线程A进入getInstance函数,并检测if条件,由于是第一次进入,value为空,if条件成立,准备创建对象实例。
b. 但是,线程A有可能被OS的调度器中断而挂起睡眠,而将控制权交给线程B。
c. 线程B同样来到if条件,发现value还是为NULL,因为线程A还没来得及构造它就已经被中断了。此时假设线程B完成了对象的创建,并顺利的返回。
d. 之后线程A被唤醒,继续执行new再次创建对象,这样一来,两个线程就构建两个对象实例,这就破坏了唯一性。
另外,还存在内存泄漏的问题,new出来的东西始终没有释放,下面是一种饿汉式的一种改进。

//代码实例(线程安全) 
std::mutex mtx;
template <typename T>
class Singleton
{
public:
    static T &getInstance()
    {
        if (value == nullptr)
        {
            lock_guard<std::mutex> lck(mtx);
            if (value == nullptr)
            {
                value= new T();
            }
        }
        return *value;
    }
private:     
	Singleton();   
	~Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;  
private:
	static T* value; 
};
template <typename T>
T *Singleton<T>::value_ = nullptr;

(2)懒汉模式实现二:局部静态变量

//代码实例(线程不安全)
template <typename T>
class Singleton
{
public:
    static T &getInstance()
    {
        static T instance;
        return instance;
    }

private:
    Singleton(){};
    Singleton(const Singleton &);
    Singleton &operator=(const Singleton &);
};
c++98:静态局部变量的实现方式也是线程不安全的。如果存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险。 对于局部静态对象的也是一样的。因为 static T instance;语句不是一个原子操作,在第一次被调用时会调用Singleton的构造函数,而如果构造函数里如果有多条初始化语句,则初始化动作可以分解为多步操作,就存在多线程竞争的问题。 为什么存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险?
c++11:线程安全的,是因为,在底层对于static静态局部变量的初始化,编译器会自动加锁和解锁。

2.饿汉模式

单例类定义的时候就进行实例化。因为main函数执行之前,全局作用域的类成员静态变量m_Instance已经初始化,故没有多线程的问题。
class Singleton
{
public:
    static Singleton *getinstance()
    {
        return &instance;
    }
private:
    static Singleton instance;
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
Singleton Singleton::instance;
优点:
	实现简单,多线程安全。

缺点:
    a. 如果存在多个单例对象且这几个单例对象相互依赖,可能会出现程序崩溃的危险。原因:对编译器来说,静态成员变量的初始化顺序和析构顺序是一个未定义的行为;具体分析在懒汉模式中也讲到了。
    b. 在程序开始时,就创建类的实例,如果Singleton对象产生很昂贵,而本身有很少使用,这种方式单从资源利用效率的角度来讲,比懒汉式单例类稍差些。但从反应时间角度来讲,则比懒汉式单例类稍好些。

使用条件:
	 a. 当肯定不会有构造和析构依赖关系的情况。
	 b. 想避免频繁加锁时的性能消耗
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值